CM3D2 Converter.model_export
1import struct 2import time 3import math 4import bpy 5import bmesh 6import mathutils 7import numpy as np 8from operator import itemgetter 9from . import common 10from . import compat 11from . import cm3d2_data 12from .translations.pgettext_functions import * 13 14 15# メインオペレーター 16@compat.BlRegister() 17class CNV_OT_export_cm3d2_model(bpy.types.Operator): 18 bl_idname = 'export_mesh.export_cm3d2_model' 19 bl_label = "CM3D2モデル (.model)" 20 bl_description = "カスタムメイド3D2のmodelファイルを書き出します" 21 bl_options = {'REGISTER'} 22 23 filepath = bpy.props.StringProperty(subtype='FILE_PATH') 24 filename_ext = ".model" 25 filter_glob = bpy.props.StringProperty(default="*.model", options={'HIDDEN'}) 26 27 scale = bpy.props.FloatProperty(name="倍率", default=0.2, min=0.01, max=100, soft_min=0.01, soft_max=100, step=10, precision=2, description="エクスポート時のメッシュ等の拡大率です") 28 29 is_backup = bpy.props.BoolProperty(name="ファイルをバックアップ", default=True, description="ファイルに上書きする場合にバックアップファイルを複製します") 30 31 version = bpy.props.EnumProperty( 32 name="ファイルバージョン", 33 items=[ 34 ('2001', '2001', 'model version 2001 (available only for com3d2)', 'NONE', 0), 35 ('2000', '2000', 'model version 2000 (com3d2 version)', 'NONE', 1), 36 ('1000', '1000', 'model version 1000 (available for cm3d2/com3d2)', 'NONE', 2), 37 ], default='1000') 38 model_name = bpy.props.StringProperty(name="model名", default="*") 39 base_bone_name = bpy.props.StringProperty(name="基点ボーン名", default="*") 40 41 items = [ 42 ('ARMATURE' , "アーマチュア", "", 'OUTLINER_OB_ARMATURE', 1), 43 ('TEXT' , "テキスト", "", 'FILE_TEXT', 2), 44 ('OBJECT_PROPERTY' , "オブジェクト内プロパティ", "", 'OBJECT_DATAMODE', 3), 45 ('ARMATURE_PROPERTY', "アーマチュア内プロパティ", "", 'ARMATURE_DATA', 4), 46 ] 47 bone_info_mode = bpy.props.EnumProperty(items=items, name="ボーン情報元", default='OBJECT_PROPERTY', description="modelファイルに必要なボーン情報をどこから引っ張ってくるか選びます") 48 49 items = [ 50 ('TEXT', "テキスト", "", 'FILE_TEXT', 1), 51 ('MATERIAL', "マテリアル", "", 'MATERIAL', 2), 52 ] 53 mate_info_mode = bpy.props.EnumProperty(items=items, name="マテリアル情報元", default='MATERIAL', description="modelファイルに必要なマテリアル情報をどこから引っ張ってくるか選びます") 54 55 is_arrange_name = bpy.props.BoolProperty(name="データ名の連番を削除", default=True, description="「○○.001」のような連番が付属したデータ名からこれらを削除します") 56 57 is_align_to_base_bone = bpy.props.BoolProperty(name="Align to Base Bone", default=True, description="Align the object to it's base bone") 58 is_convert_tris = bpy.props.BoolProperty(name="四角面を三角面に", default=True, description="四角ポリゴンを三角ポリゴンに変換してから出力します、元のメッシュには影響ありません") 59 is_split_sharp = bpy.props.BoolProperty(name="Split Sharp Edges", default=True, description="Split all edges marked as sharp.") 60 is_normalize_weight = bpy.props.BoolProperty(name="ウェイトの合計を1.0に", default=True, description="4つのウェイトの合計値が1.0になるように正規化します") 61 is_convert_bone_weight_names = bpy.props.BoolProperty(name="頂点グループ名をCM3D2用に変換", default=True, description="全ての頂点グループ名をCM3D2で使える名前にしてからエクスポートします") 62 is_clean_vertex_groups = bpy.props.BoolProperty(name="クリーンな頂点グループ", default=True, description="重みがゼロの場合、頂点グループから頂点を削除します") 63 64 is_batch = bpy.props.BoolProperty(name="バッチモード", default=False, description="モードの切替やエラー個所の選択を行いません") 65 66 export_tangent = bpy.props.BoolProperty(name="接空間情報出力", default=False, description="接空間情報(binormals, tangents)を出力する") 67 68 69 shapekey_threshold = bpy.props.FloatProperty(name="Shape Key Threshold", default=0.00100, min=0, soft_min=0.0005, max=0.01, soft_max=0.002, precision=5, description="Lower values increase accuracy and file size. Higher values truncate small changes and reduce file size.") 70 export_shapekey_normals = bpy.props.BoolProperty(name="Export Shape Key Normals", default=True, description="Export custom normals for each shape key on export.") 71 shapekey_normals_blend = bpy.props.FloatProperty(name="Shape Key Normals Blend", default=0.6, min=0, max=1, precision=3, description="Adjust the influence of shape keys on custom normals") 72 use_shapekey_colors = bpy.props.BoolProperty(name="Use Shape Key Colors", default=True, description="Use the shape key normals stored in the vertex colors instead of calculating the normals on export. (Recommend disabling if geometry was customized)") 73 74 75 @classmethod 76 def poll(cls, context): 77 ob = context.active_object 78 if ob: 79 if ob.type == 'MESH': 80 return True 81 return False 82 83 def report_cancel(self, report_message, report_type={'ERROR'}, resobj={'CANCELLED'}): 84 """エラーメッセージを出力してキャンセルオブジェクトを返す""" 85 self.report(type=report_type, message=report_message) 86 return resobj 87 88 def precheck(self, context): 89 """データの成否チェック""" 90 ob = context.active_object 91 if not ob: 92 return self.report_cancel("アクティブオブジェクトがありません") 93 if ob.type != 'MESH': 94 return self.report_cancel("メッシュオブジェクトを選択した状態で実行してください") 95 if not len(ob.material_slots): 96 return self.report_cancel("マテリアルがありません") 97 for slot in ob.material_slots: 98 if not slot.material: 99 return self.report_cancel("空のマテリアルスロットを削除してください") 100 try: 101 slot.material['shader1'] 102 slot.material['shader2'] 103 except: 104 return self.report_cancel("マテリアルに「shader1」と「shader2」という名前のカスタムプロパティを用意してください") 105 me = ob.data 106 if not me.uv_layers.active: 107 return self.report_cancel("UVがありません") 108 if 65535 < len(me.vertices): 109 return self.report_cancel("エクスポート可能な頂点数を大幅に超えています、最低でも65535未満には削減してください") 110 return None 111 112 def invoke(self, context, event): 113 res = self.precheck(context) 114 if res: 115 return res 116 ob = context.active_object 117 118 # model名とか 119 ob_names = common.remove_serial_number(ob.name, self.is_arrange_name).split('.') 120 self.model_name = ob_names[0] 121 self.base_bone_name = ob_names[1] if 2 <= len(ob_names) else 'Auto' 122 123 # ボーン情報元のデフォルトオプションを取得 124 arm_ob = ob.parent 125 for mod in ob.modifiers: 126 if mod.type == 'ARMATURE' and mod.object: 127 arm_ob = mod.object 128 if arm_ob and not arm_ob.type == 'ARMATURE': 129 arm_ob = None 130 131 info_mode_was_armature = (self.bone_info_mode == 'ARMATURE') 132 if "BoneData" in context.blend_data.texts: 133 if "LocalBoneData" in context.blend_data.texts: 134 self.bone_info_mode = 'TEXT' 135 if "BoneData:0" in ob: 136 ver = ob.get("ModelVersion") 137 if ver and ver >= 1000: 138 self.version = str(ver) 139 if "LocalBoneData:0" in ob: 140 self.bone_info_mode = 'OBJECT_PROPERTY' 141 if arm_ob: 142 if info_mode_was_armature: 143 self.bone_info_mode = 'ARMATURE' 144 else: 145 self.bone_info_mode = 'ARMATURE_PROPERTY' 146 147 # エクスポート時のデフォルトパスを取得 148 #if not self.filepath[-6:] == '.model': 149 if common.preferences().model_default_path: 150 self.filepath = common.default_cm3d2_dir(common.preferences().model_default_path, self.model_name, "model") 151 else: 152 self.filepath = common.default_cm3d2_dir(common.preferences().model_export_path, self.model_name, "model") 153 154 # バックアップ関係 155 self.is_backup = bool(common.preferences().backup_ext) 156 157 self.scale = 1.0 / common.preferences().scale 158 context.window_manager.fileselect_add(self) 159 return {'RUNNING_MODAL'} 160 161 # 'is_batch' がオンなら非表示 162 def draw(self, context): 163 self.layout.prop(self, 'scale') 164 row = self.layout.row() 165 row.prop(self, 'is_backup', icon='FILE_BACKUP') 166 if not common.preferences().backup_ext: 167 row.enabled = False 168 self.layout.prop(self, 'is_arrange_name', icon='FILE_TICK') 169 box = self.layout.box() 170 box.prop(self, 'version', icon='LINENUMBERS_ON') 171 box.prop(self, 'model_name', icon='SORTALPHA') 172 173 row = box.row() 174 row.prop(self, 'base_bone_name', icon='CONSTRAINT_BONE') 175 if self.base_bone_name == 'Auto': 176 row.enabled = False 177 178 prefs = common.preferences() 179 180 box = self.layout.box() 181 col = box.column(align=True) 182 col.label(text="ボーン情報元", icon='BONE_DATA') 183 col.prop(self, 'bone_info_mode', icon='BONE_DATA', expand=True) 184 col = box.column(align=True) 185 col.label(text="マテリアル情報元", icon='MATERIAL') 186 col.prop(self, 'mate_info_mode', icon='MATERIAL', expand=True) 187 188 box = self.layout.box() 189 box.label(text="メッシュオプション") 190 box.prop(self , 'is_align_to_base_bone', icon=compat.icon('OBJECT_ORIGIN' )) 191 box.prop(self , 'is_convert_tris' , icon=compat.icon('MESH_DATA' )) 192 box.prop(self , 'is_split_sharp' , icon=compat.icon('MOD_EDGESPLIT' )) 193 box.prop(self , 'export_tangent' , icon=compat.icon('CURVE_BEZCIRCLE')) 194 sub_box = box.box() 195 sub_box.prop(self , 'shapekey_threshold' , icon=compat.icon('SHAPEKEY_DATA' ), slider=True) 196 sub_box.prop(prefs, 'skip_shapekey' , icon=compat.icon('SHAPEKEY_DATA' ), toggle=1) 197 sub_box.prop(self , 'export_shapekey_normals', icon=compat.icon('NORMALS_VERTEX_FACE')) 198 row = sub_box.row() 199 row .prop(self , 'shapekey_normals_blend' , icon=compat.icon('MOD_NORMALEDIT' ), slider=True) 200 row.enabled = self.export_shapekey_normals 201 row = sub_box.row() 202 row .prop(self , 'use_shapekey_colors' , icon=compat.icon('GROUP_VCOL') , toggle=0) 203 row.enabled = self.export_shapekey_normals 204 sub_box = box.box() 205 sub_box.prop(self, 'is_normalize_weight', icon='MOD_VERTEX_WEIGHT') 206 sub_box.prop(self, 'is_clean_vertex_groups', icon='MOD_VERTEX_WEIGHT') 207 sub_box.prop(self, 'is_convert_bone_weight_names', icon_value=common.kiss_icon()) 208 sub_box 209 sub_box = box.box() 210 sub_box.prop(prefs, 'is_apply_modifiers', icon='MODIFIER') 211 row = sub_box.row() 212 row.prop(prefs, 'custom_normal_blend', icon='SNAP_NORMAL', slider=True) 213 row.enabled = prefs.is_apply_modifiers 214 215 def copy_and_activate_ob(self, context, ob): 216 new_ob = ob.copy() 217 new_me = ob.data.copy() 218 new_ob.data = new_me 219 compat.link(context.scene, new_ob) 220 compat.set_active(context, new_ob) 221 compat.set_select(new_ob, True) 222 return new_ob 223 224 def execute(self, context): 225 start_time = time.time() 226 prefs = common.preferences() 227 228 selected_objs = context.selected_objects 229 source_objs = [] 230 prev_mode = None 231 try: 232 ob_source = context.active_object 233 if ob_source not in selected_objs: 234 selected_objs.append(ob_source) # luvoid : Fix error where object is active but not selected 235 ob_name = ob_source.name 236 ob_main = None 237 if self.is_batch: 238 # アクティブオブジェクトを1つコピーするだけでjoinしない 239 source_objs.append(ob_source) 240 compat.set_select(ob_source, False) 241 ob_main = self.copy_and_activate_ob(context, ob_source) 242 243 if prefs.is_apply_modifiers and bpy.ops.object.forced_modifier_apply.poll(context): 244 bpy.ops.object.forced_modifier_apply(is_applies=[True for i in range(32)]) 245 else: 246 selected_count = 0 247 # 選択されたMESHオブジェクトをコピーしてjoin 248 # 必要に応じて、モディファイアの強制適用を行う 249 for selected in selected_objs: 250 source_objs.append(selected) 251 252 compat.set_select(selected, False) 253 254 if selected.type == 'MESH': 255 ob_created = self.copy_and_activate_ob(context, selected) 256 if selected == ob_source: 257 ob_main = ob_created 258 if prefs.is_apply_modifiers: 259 bpy.ops.object.forced_modifier_apply(apply_viewport_visible=True) 260 261 selected_count += 1 262 263 mode = context.active_object.mode 264 if mode != 'OBJECT': 265 prev_mode = mode 266 bpy.ops.object.mode_set(mode='OBJECT') 267 268 if selected_count > 1: 269 if ob_main: 270 compat.set_active(context, ob_main) 271 bpy.ops.object.join() 272 self.report(type={'INFO'}, message=f_tip_("{}個のオブジェクトをマージしました", selected_count)) 273 274 ret = self.export(context, ob_main) 275 if 'FINISHED' not in ret: 276 return ret 277 278 context.window_manager.progress_update(10) 279 diff_time = time.time() - start_time 280 self.report(type={'INFO'}, message=f_tip_("modelのエクスポートが完了しました。{:.2f} 秒 file={}", diff_time, self.filepath)) 281 return ret 282 finally: 283 # 作業データの破棄(コピーデータを削除、選択状態の復元、アクティブオブジェクト、モードの復元) 284 if ob_main: 285 common.remove_data(ob_main) 286 # me_copied = ob_main.data 287 # context.blend_data.objects.remove(ob_main, do_unlink=True) 288 # context.blend_data.meshes.remove(me_copied, do_unlink=True) 289 290 for obj in source_objs: 291 compat.set_select(obj, True) 292 293 if ob_source: 294 # TODO 元のオブジェクトをアクティブに戻す 295 if ob_name in bpy.data.objects: 296 compat.set_active(context, ob_source) 297 298 if prev_mode: 299 bpy.ops.object.mode_set(mode=prev_mode) 300 301 def export(self, context, ob): 302 """モデルファイルを出力""" 303 prefs = common.preferences() 304 305 if not self.is_batch: 306 prefs.model_export_path = self.filepath 307 prefs.scale = 1.0 / self.scale 308 309 context.window_manager.progress_begin(0, 10) 310 context.window_manager.progress_update(0) 311 312 res = self.precheck(context) 313 if res: 314 return res 315 me = ob.data 316 317 if ob.active_shape_key_index != 0: 318 ob.active_shape_key_index = 0 319 me.update() 320 321 # データの成否チェック 322 if self.bone_info_mode == 'ARMATURE': 323 arm_ob = ob.parent 324 if arm_ob and arm_ob.type != 'ARMATURE': 325 return self.report_cancel("メッシュオブジェクトの親がアーマチュアではありません") 326 if not arm_ob: 327 try: 328 arm_ob = next(mod for mod in ob.modifiers if mod.type == 'ARMATURE' and mod.object) 329 except StopIteration: 330 return self.report_cancel("アーマチュアが見つかりません、親にするかモディファイアにして下さい") 331 arm_ob = arm_ob.object 332 elif self.bone_info_mode == 'TEXT': 333 if "BoneData" not in context.blend_data.texts: 334 return self.report_cancel("テキスト「BoneData」が見つかりません、中止します") 335 if "LocalBoneData" not in context.blend_data.texts: 336 return self.report_cancel("テキスト「LocalBoneData」が見つかりません、中止します") 337 elif self.bone_info_mode == 'OBJECT_PROPERTY': 338 if "BoneData:0" not in ob: 339 return self.report_cancel("オブジェクトのカスタムプロパティにボーン情報がありません") 340 if "LocalBoneData:0" not in ob: 341 return self.report_cancel("オブジェクトのカスタムプロパティにボーン情報がありません") 342 elif self.bone_info_mode == 'ARMATURE_PROPERTY': 343 arm_ob = ob.parent 344 if arm_ob and arm_ob.type != 'ARMATURE': 345 return self.report_cancel("メッシュオブジェクトの親がアーマチュアではありません") 346 if not arm_ob: 347 try: 348 arm_ob = next(mod for mod in ob.modifiers if mod.type == 'ARMATURE' and mod.object) 349 except StopIteration: 350 return self.report_cancel("アーマチュアが見つかりません、親にするかモディファイアにして下さい") 351 arm_ob = arm_ob.object 352 if "BoneData:0" not in arm_ob.data: 353 return self.report_cancel("アーマチュアのカスタムプロパティにボーン情報がありません") 354 if "LocalBoneData:0" not in arm_ob.data: 355 return self.report_cancel("アーマチュアのカスタムプロパティにボーン情報がありません") 356 else: 357 return self.report_cancel("ボーン情報元のモードがおかしいです") 358 359 if self.mate_info_mode == 'TEXT': 360 for index, slot in enumerate(ob.material_slots): 361 if "Material:" + str(index) not in context.blend_data.texts: 362 return self.report_cancel("マテリアル情報元のテキストが足りません") 363 context.window_manager.progress_update(1) 364 365 # model名とか 366 ob_names = common.remove_serial_number(ob.name, self.is_arrange_name).split('.') 367 if self.model_name == '*': 368 self.model_name = ob_names[0] 369 if self.base_bone_name == '*': 370 self.base_bone_name = ob_names[1] if 2 <= len(ob_names) else 'Auto' 371 372 # BoneData情報読み込み 373 base_bone_candidate = None 374 bone_data = [] 375 if self.bone_info_mode == 'ARMATURE': 376 bone_data = self.armature_bone_data_parser(context, arm_ob) 377 base_bone_candidate = arm_ob.data['BaseBone'] 378 elif self.bone_info_mode == 'TEXT': 379 bone_data_text = context.blend_data.texts["BoneData"] 380 if 'BaseBone' in bone_data_text: 381 base_bone_candidate = bone_data_text['BaseBone'] 382 bone_data = self.bone_data_parser(l.body for l in bone_data_text.lines) 383 elif self.bone_info_mode in ['OBJECT_PROPERTY', 'ARMATURE_PROPERTY']: 384 target = ob if self.bone_info_mode == 'OBJECT_PROPERTY' else arm_ob.data 385 if 'BaseBone' in target: 386 base_bone_candidate = target['BaseBone'] 387 bone_data = self.bone_data_parser(self.indexed_data_generator(target, prefix="BoneData:")) 388 if len(bone_data) <= 0: 389 return self.report_cancel("テキスト「BoneData」に有効なデータがありません") 390 391 if self.base_bone_name not in (b['name'] for b in bone_data): 392 if base_bone_candidate and self.base_bone_name == 'Auto': 393 self.base_bone_name = base_bone_candidate 394 else: 395 return self.report_cancel("基点ボーンが存在しません") 396 bone_name_indices = {bone['name']: index for index, bone in enumerate(bone_data)} 397 context.window_manager.progress_update(2) 398 399 if self.is_align_to_base_bone: 400 bpy.ops.object.align_to_cm3d2_base_bone(scale=1.0/self.scale, is_preserve_mesh=True, bone_info_mode=self.bone_info_mode) 401 me.update() 402 403 if self.is_split_sharp: 404 bpy.ops.object.mode_set(mode='EDIT') 405 bpy.ops.mesh.split_sharp() 406 bpy.ops.object.mode_set(mode='OBJECT') 407 408 # LocalBoneData情報読み込み 409 local_bone_data = [] 410 if self.bone_info_mode == 'ARMATURE': 411 local_bone_data = self.armature_local_bone_data_parser(arm_ob) 412 elif self.bone_info_mode == 'TEXT': 413 local_bone_data_text = context.blend_data.texts["LocalBoneData"] 414 local_bone_data = self.local_bone_data_parser(l.body for l in local_bone_data_text.lines) 415 elif self.bone_info_mode in ['OBJECT_PROPERTY', 'ARMATURE_PROPERTY']: 416 target = ob if self.bone_info_mode == 'OBJECT_PROPERTY' else arm_ob.data 417 local_bone_data = self.local_bone_data_parser(self.indexed_data_generator(target, prefix="LocalBoneData:")) 418 if len(local_bone_data) <= 0: 419 return self.report_cancel("テキスト「LocalBoneData」に有効なデータがありません") 420 local_bone_name_indices = {bone['name']: index for index, bone in enumerate(local_bone_data)} 421 context.window_manager.progress_update(3) 422 423 used_local_bone = {index: False for index, bone in enumerate(local_bone_data)} 424 425 # ウェイト情報読み込み 426 vertices = [] 427 is_over_one = 0 428 is_under_one = 0 429 is_in_too_many = 0 430 for i, vert in enumerate(me.vertices): 431 vgs = [] 432 for vg in vert.groups: 433 if len(ob.vertex_groups) <= vg.group: # Apparently a vertex can be assigned to a non-existent group. 434 continue 435 name = common.encode_bone_name(ob.vertex_groups[vg.group].name, self.is_convert_bone_weight_names) 436 index = local_bone_name_indices.get(name, -1) 437 if index >= 0 and (vg.weight > 0.0 or not self.is_clean_vertex_groups): 438 vgs.append([index, vg.weight]) 439 # luvoid : track used bones 440 used_local_bone[index] = True 441 boneindex = bone_name_indices.get(name, -1) 442 while boneindex >= 0: 443 parent = bone_data[boneindex] 444 localindex = local_bone_name_indices.get(parent['name'], -1) 445 # could check for `localindex == -1` here, but it's prescence may be useful in determing if the local bones resolve back to some root 446 used_local_bone[localindex] = True 447 boneindex = parent['parent_index'] 448 if len(vgs) == 0: 449 if not self.is_batch: 450 self.select_no_weight_vertices(context, local_bone_name_indices) 451 return self.report_cancel("ウェイトが割り当てられていない頂点が見つかりました、中止します") 452 if len(vgs) > 4: 453 is_in_too_many += 1 454 vgs = sorted(vgs, key=itemgetter(1), reverse=True)[0:4] 455 total = sum(vg[1] for vg in vgs) 456 if self.is_normalize_weight: 457 for vg in vgs: 458 vg[1] /= total 459 else: 460 if 1.01 < total: 461 is_over_one += 1 462 elif total < 0.99: 463 is_under_one += 1 464 if len(vgs) < 4: 465 vgs += [(0, 0.0)] * (4 - len(vgs)) 466 vertices.append({ 467 'index': vert.index, 468 'face_indexs': list(map(itemgetter(0), vgs)), 469 'weights': list(map(itemgetter(1), vgs)), 470 }) 471 472 if 1 <= is_over_one: 473 self.report(type={'WARNING'}, message=f_tip_("ウェイトの合計が1.0を超えている頂点が見つかりました。正規化してください。超過している頂点の数:{}", is_over_one)) 474 if 1 <= is_under_one: 475 self.report(type={'WARNING'}, message=f_tip_("ウェイトの合計が1.0未満の頂点が見つかりました。正規化してください。不足している頂点の数:{}", is_under_one)) 476 477 # luvoid : warn that there are vertices in too many vertex groups 478 if is_in_too_many > 0: 479 self.report(type={'WARNING'}, message=f_tip_("4つを超える頂点グループにある頂点が見つかりました。頂点グループをクリーンアップしてください。不足している頂点の数:{}", is_in_too_many)) 480 481 # luvoid : check for unused local bones that the game will delete 482 is_deleted = 0 483 deleted_names = "The game will delete these local bones" 484 for index, is_used in used_local_bone.items(): 485 print(index, is_used) 486 if is_used == False: 487 is_deleted += 1 488 deleted_names = deleted_names + '\n' + local_bone_data[index]['name'] 489 elif is_used == True: 490 pass 491 else: 492 print(f_tip_("Unexpected: used_local_bone[{key}] == {value} when len(used_local_bone) == {length}", key=index, value=is_used, length=len(used_local_bone))) 493 self.report(type={'WARNING'}, message=f_tip_("Could not find whether bone with index {index} was used. See console for more info.", index=i)) 494 if is_deleted > 0: 495 self.report(type={'WARNING'}, message=f_tip_("頂点が割り当てられていない{num}つのローカルボーンが見つかりました。 詳細については、ログを参照してください。", num=is_deleted)) 496 self.report(type={'INFO'}, message=deleted_names) 497 498 context.window_manager.progress_update(4) 499 500 501 try: 502 writer = common.open_temporary(self.filepath, 'wb', is_backup=self.is_backup) 503 except: 504 self.report(type={'ERROR'}, message=f_tip_("ファイルを開くのに失敗しました、アクセス不可かファイルが存在しません。file={}", self.filepath)) 505 return {'CANCELLED'} 506 507 model_datas = { 508 'bone_data': bone_data, 509 'local_bone_data': local_bone_data, 510 'vertices': vertices, 511 } 512 try: 513 with writer: 514 self.write_model(context, ob, writer, **model_datas) 515 except common.CM3D2ExportException as e: 516 self.report(type={'ERROR'}, message=str(e)) 517 return {'CANCELLED'} 518 519 return {'FINISHED'} 520 521 def write_model(self, context, ob, writer, bone_data=[], local_bone_data=[], vertices=[]): 522 """モデルデータをファイルオブジェクトに書き込む""" 523 me = ob.data 524 prefs = common.preferences() 525 526 # ファイル先頭 527 common.write_str(writer, 'CM3D2_MESH') 528 self.version_num = int(self.version) 529 writer.write(struct.pack('<i', self.version_num)) 530 531 common.write_str(writer, self.model_name) 532 common.write_str(writer, self.base_bone_name) 533 534 # ボーン情報書き出し 535 writer.write(struct.pack('<i', len(bone_data))) 536 for bone in bone_data: 537 common.write_str(writer, bone['name']) 538 writer.write(struct.pack('<b', bone['scl'])) 539 context.window_manager.progress_update(3.3) 540 for bone in bone_data: 541 writer.write(struct.pack('<i', bone['parent_index'])) 542 context.window_manager.progress_update(3.7) 543 for bone in bone_data: 544 writer.write(struct.pack('<3f', bone['co'][0], bone['co'][1], bone['co'][2])) 545 writer.write(struct.pack('<4f', bone['rot'][1], bone['rot'][2], bone['rot'][3], bone['rot'][0])) 546 if self.version_num >= 2001: 547 use_scale = ('scale' in bone) 548 writer.write(struct.pack('<b', use_scale)) 549 if use_scale: 550 bone_scale = bone['scale'] 551 writer.write(struct.pack('<3f', bone_scale[0], bone_scale[1], bone_scale[2])) 552 context.window_manager.progress_update(4) 553 554 # 正しい頂点数などを取得 555 bm = bmesh.new() 556 bm.from_mesh(me) 557 uv_lay = bm.loops.layers.uv.active 558 vert_uvs = [] 559 vert_uvs_append = vert_uvs.append 560 vert_iuv = {} 561 vert_indices = {} 562 vert_count = 0 563 for vert in bm.verts: 564 vert_uv = [] 565 vert_uvs_append(vert_uv) 566 for loop in vert.link_loops: 567 uv = loop[uv_lay].uv 568 if uv not in vert_uv: 569 vert_uv.append(uv) 570 vert_iuv[hash((vert.index, uv.x, uv.y))] = vert_count 571 vert_indices[vert.index] = vert_count 572 vert_count += 1 573 if 65535 < vert_count: 574 raise common.CM3D2ExportException(f_tip_("頂点数がまだ多いです (現在{}頂点)。あと{}頂点以上減らしてください、中止します", vert_count, vert_count - 65535)) 575 context.window_manager.progress_update(5) 576 577 writer.write(struct.pack('<2i', vert_count, len(ob.material_slots))) 578 579 # ローカルボーン情報を書き出し 580 writer.write(struct.pack('<i', len(local_bone_data))) 581 for bone in local_bone_data: 582 common.write_str(writer, bone['name']) 583 context.window_manager.progress_update(5.3) 584 for bone in local_bone_data: 585 for f in bone['matrix']: 586 writer.write(struct.pack('<f', f)) 587 context.window_manager.progress_update(5.7) 588 589 # カスタム法線情報を取得 590 if me.has_custom_normals: 591 custom_normals = [mathutils.Vector() for i in range(len(me.vertices))] 592 me.calc_normals_split() 593 for loop in me.loops: 594 custom_normals[loop.vertex_index] += loop.normal 595 for no in custom_normals: 596 no.normalize() 597 else: 598 custom_normals = None 599 600 cm_verts = [] 601 cm_norms = [] 602 cm_uvs = [] 603 # 頂点情報を書き出し 604 for i, vert in enumerate(bm.verts): 605 co = compat.convert_bl_to_cm_space( vert.co * self.scale ) 606 if me.has_custom_normals: 607 no = custom_normals[vert.index] 608 else: 609 no = vert.normal.copy() 610 no = compat.convert_bl_to_cm_space( no ) 611 for uv in vert_uvs[i]: 612 cm_verts.append(co) 613 cm_norms.append(no) 614 cm_uvs.append(uv) 615 writer.write(struct.pack('<3f', co.x, co.y, co.z)) 616 writer.write(struct.pack('<3f', no.x, no.y, no.z)) 617 writer.write(struct.pack('<2f', uv.x, uv.y)) 618 context.window_manager.progress_update(6) 619 620 cm_tris = self.parse_triangles(bm, ob, uv_lay, vert_iuv, vert_indices) 621 622 # 接空間情報を書き出し 623 if self.export_tangent: 624 tangents = self.calc_tangents(cm_tris, cm_verts, cm_norms, cm_uvs) 625 writer.write(struct.pack('<i', len(tangents))) 626 for t in tangents: 627 writer.write(struct.pack('<4f', *t)) 628 else: 629 writer.write(struct.pack('<i', 0)) 630 631 # ウェイト情報を書き出し 632 for vert in vertices: 633 for uv in vert_uvs[vert['index']]: 634 writer.write(struct.pack('<4H', *vert['face_indexs'])) 635 writer.write(struct.pack('<4f', *vert['weights'])) 636 context.window_manager.progress_update(7) 637 638 # 面情報を書き出し 639 for tri in cm_tris: 640 writer.write(struct.pack('<i', len(tri))) 641 for vert_index in tri: 642 writer.write(struct.pack('<H', vert_index)) 643 context.window_manager.progress_update(8) 644 645 # マテリアルを書き出し 646 writer.write(struct.pack('<i', len(ob.material_slots))) 647 for slot_index, slot in enumerate(ob.material_slots): 648 if self.mate_info_mode == 'MATERIAL': 649 mat_data = cm3d2_data.MaterialHandler.parse_mate(slot.material, self.is_arrange_name) 650 mat_data.write(writer, write_header=False) 651 652 elif self.mate_info_mode == 'TEXT': 653 text = context.blend_data.texts["Material:" + str(slot_index)].as_string() 654 mat_data = cm3d2_data.MaterialHandler.parse_text(slot.material, self.is_arrange_name) 655 mat_data.write(writer, write_header=False) 656 657 context.window_manager.progress_update(9) 658 659 # モーフを書き出し 660 if me.shape_keys and len(me.shape_keys.key_blocks) >= 2: 661 try: 662 self.write_shapekeys(context, ob, writer, vert_uvs, custom_normals) 663 finally: 664 print("FINISHED SHAPE KEYS WRITE") 665 pass 666 common.write_str(writer, 'end') 667 668 def write_shapekeys(self, context, ob, writer, vert_uvs, custom_normals=None): 669 # モーフを書き出し 670 me = ob.data 671 prefs = common.preferences() 672 673 is_use_attributes = (not compat.IS_LEGACY and bpy.app.version >= (2,92)) 674 675 loops_vert_index = np.empty((len(me.loops)), dtype=int) 676 me.loops.foreach_get('vertex_index', loops_vert_index.ravel()) 677 678 def find_normals_attribute(name) -> (bpy.types.Attribute, bool): 679 if is_use_attributes: 680 normals_color = me.attributes[name] if name in me.attributes.keys() else None 681 attribute_is_color = (not normals_color is None) and normals_color.data_type in {'BYTE_COLOR', 'FLOAT_COLOR'} 682 else: 683 normals_color = me.vertex_colors[name] if name in me.vertex_colors.keys() else None 684 attribute_is_color = True 685 return normals_color, attribute_is_color 686 687 if self.use_shapekey_colors: 688 static_attribute_colors = np.empty((len(me.loops), 4), dtype=float) 689 color_offset = np.array([[0.5,0.5,0.5]]) 690 loops_per_vertex = np.zeros((len(me.vertices))) 691 for loop in me.loops: 692 loops_per_vertex[loop.vertex_index] += 1 693 loops_per_vertex_reciprocal = np.reciprocal(loops_per_vertex, out=loops_per_vertex).reshape((len(me.vertices), 1)) 694 def get_sk_delta_normals_from_attribute(attribute, is_color, out): 695 if is_color: 696 attribute.data.foreach_get('color', static_attribute_colors.ravel()) 697 loop_delta_normals = static_attribute_colors[:,:3] 698 loop_delta_normals -= color_offset 699 loop_delta_normals *= 2 700 else: 701 loop_delta_normals = static_attribute_colors[:,:3] 702 attribute.data.foreach_get('vector', loop_delta_normals.ravel()) 703 704 vert_delta_normals = out 705 vert_delta_normals.fill(0) 706 707 # for loop in me.loops: vert_delta_normals[loop.vertex_index] += loop_delta_normals[loop.index] 708 np.add.at(vert_delta_normals, loops_vert_index, loop_delta_normals) # XXX Slower but handles edge cases better 709 #vert_delta_normals[loops_vert_index] += loop_delta_normals # XXX Only first loop's value will be kept 710 711 # for delta_normal in vert_delta_normals: delta_normal /= loops_per_vertex[vert.index] 712 vert_delta_normals *= loops_per_vertex_reciprocal 713 714 return out #.tolist() 715 716 if me.has_custom_normals: 717 basis_custom_normals = np.array(custom_normals, dtype=float) 718 static_loop_normals = np.empty((len(me.loops), 3), dtype=float) 719 static_vert_lengths = np.empty((len(me.vertices), 1), dtype=float) 720 def get_sk_delta_normals_from_custom_normals(shape_key, out): 721 vert_custom_normals = out 722 vert_custom_normals.fill(0) 723 724 loop_custom_normals = static_loop_normals 725 np.copyto(loop_custom_normals.ravel(), shape_key.normals_split_get()) 726 727 # for loop in me.loops: vert_delta_normals[loop.vertex_index] += loop_delta_normals[loop.index] 728 if not self.is_split_sharp: 729 # XXX Slower 730 np.add.at(vert_custom_normals, loops_vert_index, loop_custom_normals) 731 vert_len_sq = get_lengths_squared(vert_custom_normals, out=static_vert_lengths) 732 vert_len = np.sqrt(vert_len_sq, out=vert_len_sq) 733 np.reciprocal(vert_len, out=vert_len) 734 vert_custom_normals *= vert_len #.reshape((*vert_len.shape, 1)) 735 else: 736 # loop normals should be the same per-vertex unless there is a sharp edge 737 # or a flat shaded face, but all sharp edges were split, so this method is fine 738 # (and Flat shaded faces just won't be supported) 739 vert_custom_normals[loops_vert_index] += loop_custom_normals # Only first loop's value will be kept 740 741 vert_custom_normals -= basis_custom_normals 742 return out 743 744 if not me.has_custom_normals: 745 basis_normals = np.empty((len(me.vertices), 3), dtype=float) 746 me.vertices.foreach_get('normal', basis_normals.ravel()) 747 def get_sk_delta_normals_from_normals(shape_key, out): 748 vert_normals = out 749 np.copyto(vert_normals.ravel(), shape_key.normals_vertex_get()) 750 vert_delta_normals = np.subtract(vert_normals, basis_normals, out=out) 751 return out 752 753 basis_co = np.empty((len(me.vertices), 3), dtype=float) 754 me.vertices.foreach_get('co', basis_co.ravel()) 755 def get_sk_delta_coordinates(shape_key, out): 756 delta_coordinates = out 757 shape_key.data.foreach_get('co', delta_coordinates.ravel()) 758 delta_coordinates -= basis_co 759 return out 760 761 static_array_sq = np.empty((len(me.vertices), 3), dtype=float) 762 def get_lengths_squared(vectors, out): 763 np.power(vectors, 2, out=static_array_sq) 764 np.sum(static_array_sq, axis=1, out=out.ravel()) 765 return out 766 767 def write_morph(morph, name): 768 common.write_str(writer, 'morph') 769 common.write_str(writer, name) 770 writer.write(struct.pack('<i', len(morph))) 771 for v_index, vec, normal in morph: 772 vec = compat.convert_bl_to_cm_space(vec ) 773 normal = compat.convert_bl_to_cm_space(normal) 774 writer.write(struct.pack('<H', v_index)) 775 writer.write(struct.pack('<3f', *vec[:3])) 776 writer.write(struct.pack('<3f', *normal[:3])) 777 778 # accessing operator properties via "self.x" is SLOW, so store some here 779 self__export_shapekey_normals = self.export_shapekey_normals 780 self__use_shapekey_colors = self.use_shapekey_colors 781 self__shapekey_normals_blend = self.shapekey_normals_blend 782 self__scale = self.scale 783 784 co_diff_threshold = self.shapekey_threshold / 5 785 co_diff_threshold_squared = co_diff_threshold * co_diff_threshold 786 no_diff_threshold = self.shapekey_threshold * 10 787 no_diff_threshold_squared = no_diff_threshold * no_diff_threshold 788 789 # shared arrays 790 delta_coordinates = np.empty((len(me.vertices), 3), dtype=float) 791 vert_delta_normals = np.empty((len(me.vertices), 3), dtype=float) 792 loop_delta_normals = np.empty((len(me.loops ), 3), dtype=float) 793 794 delta_co_lensq = np.empty((len(me.vertices)), dtype=float) 795 delta_no_lensq = np.empty((len(me.vertices)), dtype=float) 796 797 if not self.export_shapekey_normals: 798 vert_delta_normals.fill(0) 799 delta_no_lensq.fill(0) 800 801 # HEAVY LOOP 802 for shape_key in me.shape_keys.key_blocks[1:]: 803 morph = [] 804 805 if self__export_shapekey_normals and self__use_shapekey_colors: 806 normals_color, attrubute_is_color = find_normals_attribute(f'{shape_key.name}_delta_normals') 807 808 if self__export_shapekey_normals: 809 if self__use_shapekey_colors and not normals_color is None: 810 sk_delta_normals = get_sk_delta_normals_from_attribute(normals_color, attrubute_is_color, out=vert_delta_normals) 811 elif me.has_custom_normals: 812 sk_delta_normals = get_sk_delta_normals_from_custom_normals(shape_key, out=vert_delta_normals) 813 sk_delta_normals *= self__shapekey_normals_blend 814 else: 815 sk_delta_normals = get_sk_delta_normals_from_normals(shape_key, out=vert_delta_normals) 816 sk_delta_normals *= self__shapekey_normals_blend 817 818 sk_no_lensq = get_lengths_squared(sk_delta_normals, out=delta_no_lensq) 819 else: 820 sk_delta_normals = vert_delta_normals 821 sk_no_lensq = delta_no_lensq 822 823 sk_co_diffs = get_sk_delta_coordinates(shape_key, out=delta_coordinates) 824 sk_co_diffs *= self__scale # scale before getting lengths 825 sk_co_lensq = get_lengths_squared(sk_co_diffs, out=delta_co_lensq) 826 827 # SUPER HEAVY LOOP 828 outvert_index = 0 829 for i in range(len(me.vertices)): 830 if sk_co_lensq[i] >= co_diff_threshold_squared or sk_no_lensq[i] >= no_diff_threshold_squared: 831 morph += [ (outvert_index+j, sk_co_diffs[i], sk_delta_normals[i]) for j in range(len(vert_uvs[i])) ] 832 else: 833 # ignore because change is too small (greatly lowers file size) 834 pass 835 outvert_index += len(vert_uvs[i]) 836 837 if prefs.skip_shapekey and not len(morph): 838 continue 839 else: 840 write_morph(morph, shape_key.name) 841 842 def write_tangents(self, writer, me): 843 if len(me.uv_layers) < 1: 844 return 845 846 num_loops = len(me.loops) 847 848 def parse_triangles(self, bm, ob, uv_lay, vert_iuv, vert_indices): 849 def vert_index_from_loops(loops): 850 """vert_index generator""" 851 for loop in loops: 852 uv = loop[uv_lay].uv 853 v_index = loop.vert.index 854 vert_index = vert_iuv.get(hash((v_index, uv.x, uv.y))) 855 if vert_index is None: 856 vert_index = vert_indices.get(v_index, 0) 857 yield vert_index 858 859 triangles = [] 860 for mate_index, slot in enumerate(ob.material_slots): 861 tris_faces = [] 862 for face in bm.faces: 863 if face.material_index != mate_index: 864 continue 865 if len(face.verts) == 3: 866 tris_faces.extend(vert_index_from_loops(reversed(face.loops))) 867 elif len(face.verts) == 4 and self.is_convert_tris: 868 v1 = face.loops[0].vert.co - face.loops[2].vert.co 869 v2 = face.loops[1].vert.co - face.loops[3].vert.co 870 if v1.length < v2.length: 871 f1 = [0, 1, 2] 872 f2 = [0, 2, 3] 873 else: 874 f1 = [0, 1, 3] 875 f2 = [1, 2, 3] 876 faces, faces2 = [], [] 877 for i, vert_index in enumerate(vert_index_from_loops(reversed(face.loops))): 878 if i in f1: 879 faces.append(vert_index) 880 if i in f2: 881 faces2.append(vert_index) 882 tris_faces.extend(faces) 883 tris_faces.extend(faces2) 884 elif 5 <= len(face.verts) and self.is_convert_tris: 885 face_count = len(face.verts) - 2 886 887 tris = [] 888 seek_min, seek_max = 0, len(face.verts) - 1 889 for i in range(face_count): 890 if not i % 2: 891 tris.append([seek_min, seek_min + 1, seek_max]) 892 seek_min += 1 893 else: 894 tris.append([seek_min, seek_max - 1, seek_max]) 895 seek_max -= 1 896 897 tris_indexs = [[] for _ in range(len(tris))] 898 for i, vert_index in enumerate(vert_index_from_loops(reversed(face.loops))): 899 for tris_index, points in enumerate(tris): 900 if i in points: 901 tris_indexs[tris_index].append(vert_index) 902 903 tris_faces.extend(p for ps in tris_indexs for p in ps) 904 905 triangles.append(tris_faces) 906 return triangles 907 908 def calc_tangents(self, cm_tris, cm_verts, cm_norms, cm_uvs): 909 count = len(cm_verts) 910 tan1 = [None] * count 911 tan2 = [None] * count 912 for i in range(0, count): 913 tan1[i] = mathutils.Vector((0, 0, 0)) 914 tan2[i] = mathutils.Vector((0, 0, 0)) 915 916 for tris in cm_tris: 917 tri_len = len(tris) 918 tri_idx = 0 919 while tri_idx < tri_len: 920 i1, i2, i3 = tris[tri_idx], tris[tri_idx + 1], tris[tri_idx + 2] 921 v1, v2, v3 = cm_verts[i1], cm_verts[i2], cm_verts[i3] 922 w1, w2, w3 = cm_uvs[i1], cm_uvs[i2], cm_uvs[i3] 923 924 a1 = v2 - v1 925 a2 = v3 - v1 926 s1 = w2 - w1 927 s2 = w3 - w1 928 929 r_inverse = (s1.x * s2.y - s2.x * s1.y) 930 931 if r_inverse != 0: 932 # print("i1 = {i1} i2 = {i2} i3 = {i3}".format(i1=i1, i2=i2, i3=i3)) 933 # print("v1 = {v1} v2 = {v2} v3 = {v3}".format(v1=v1, v2=v2, v3=v3)) 934 # print("w1 = {w1} w2 = {w2} w3 = {w3}".format(w1=w1, w2=w2, w3=w3)) 935 936 # print("a1 = {a1} a2 = {a2}".format(a1=a1, a2=a2)) 937 # print("s1 = {s1} s2 = {s2}".format(s1=s1, s2=s2)) 938 939 # print("r_inverse = ({s1x} * {s2y} - {s2x} * {s1y}) = {r_inverse}".format(r_inverse=r_inverse, s1x=s1.x, s1y=s1.y, s2x=s2.x, s2y=s2.y)) 940 941 r = 1.0 / r_inverse 942 sdir = mathutils.Vector(((s2.y * a1.x - s1.y * a2.x) * r, (s2.y * a1.y - s1.y * a2.y) * r, (s2.y * a1.z - s1.y * a2.z) * r)) 943 tan1[i1] += sdir 944 tan1[i2] += sdir 945 tan1[i3] += sdir 946 947 tdir = mathutils.Vector(((s1.x * a2.x - s2.x * a1.x) * r, (s1.x * a2.y - s2.x * a1.y) * r, (s1.x * a2.z - s2.x * a1.z) * r)) 948 tan2[i1] += tdir 949 tan2[i2] += tdir 950 tan2[i3] += tdir 951 952 tri_idx += 3 953 954 tangents = [None] * count 955 for i in range(0, count): 956 n = cm_norms[i] 957 ti = tan1[i] 958 t = (ti - n * n.dot(ti)).normalized() 959 960 c = n.cross(ti) 961 val = c.dot(tan2[i]) 962 w = 1.0 if val < 0 else -1.0 963 tangents[i] = (-t.x, t.y, t.z, w) 964 965 return tangents 966 967 def select_no_weight_vertices(self, context, local_bone_name_indices): 968 """ウェイトが割り当てられていない頂点を選択する""" 969 ob = context.active_object 970 me = ob.data 971 bpy.ops.object.mode_set(mode='EDIT') 972 bpy.ops.mesh.select_all(action='SELECT') 973 #bpy.ops.object.mode_set(mode='OBJECT') 974 context.tool_settings.mesh_select_mode = (True, False, False) 975 for vert in me.vertices: 976 for vg in vert.groups: 977 if len(ob.vertex_groups) <= vg.group: # Apparently a vertex can be assigned to a non-existent group. 978 continue 979 name = common.encode_bone_name(ob.vertex_groups[vg.group].name, self.is_convert_bone_weight_names) 980 if name in local_bone_name_indices and 0.0 < vg.weight: 981 vert.select = False 982 break 983 bpy.ops.object.mode_set(mode='EDIT') 984 985 def armature_bone_data_parser(self, context, ob): 986 """アーマチュアを解析してBoneDataを返す""" 987 arm = ob.data 988 989 pre_active = compat.get_active(context) 990 pre_mode = ob.mode 991 992 compat.set_active(context, ob) 993 bpy.ops.object.mode_set(mode='EDIT') 994 995 bones = [] 996 bone_name_indices = {} 997 already_bone_names = [] 998 bones_queue = arm.edit_bones[:] 999 while len(bones_queue): 1000 bone = bones_queue.pop(0) 1001 1002 if not bone.parent: 1003 already_bone_names.append(bone.name) 1004 bones.append(bone) 1005 bone_name_indices[bone.name] = len(bone_name_indices) 1006 continue 1007 elif bone.parent.name in already_bone_names: 1008 already_bone_names.append(bone.name) 1009 bones.append(bone) 1010 bone_name_indices[bone.name] = len(bone_name_indices) 1011 continue 1012 1013 bones_queue.append(bone) 1014 1015 bone_data = [] 1016 for bone in bones: 1017 1018 # Also check for UnknownFlag for backwards compatibility 1019 is_scl_bone = bone['cm3d2_scl_bone'] if 'cm3d2_scl_bone' in bone \ 1020 else bone['UnknownFlag'] if 'UnknownFlag' in bone \ 1021 else 0 1022 parent_index = bone_name_indices[bone.parent.name] if bone.parent else -1 1023 1024 mat = bone.matrix.copy() 1025 1026 if bone.parent: 1027 mat = compat.convert_bl_to_cm_bone_rotation(mat) 1028 mat = compat.mul(bone.parent.matrix.inverted(), mat) 1029 mat = compat.convert_bl_to_cm_bone_space(mat) 1030 else: 1031 mat = compat.convert_bl_to_cm_bone_rotation(mat) 1032 mat = compat.convert_bl_to_cm_space(mat) 1033 1034 co = mat.to_translation() * self.scale 1035 rot = mat.to_quaternion() 1036 1037 #if bone.parent: 1038 # co.x, co.y, co.z = -co.y, -co.x, co.z 1039 # rot.w, rot.x, rot.y, rot.z = rot.w, rot.y, rot.x, -rot.z 1040 #else: 1041 # co.x, co.y, co.z = -co.x, co.z, -co.y 1042 # 1043 # fix_quat = compat.Z_UP_TO_Y_UP_QUAT #mathutils.Euler((0, 0, math.radians(-90)), 'XYZ').to_quaternion() 1044 # fix_quat2 = compat.BLEND_TO_OPENGL_QUAT #mathutils.Euler((math.radians(-90), 0, 0), 'XYZ').to_quaternion() 1045 # rot = compat.mul3(rot, fix_quat, fix_quat2) 1046 # #rot = compat.mul3(fix_quat2, rot, fix_quat) 1047 # 1048 # rot.w, rot.x, rot.y, rot.z = -rot.y, -rot.z, -rot.x, rot.w 1049 1050 # luvoid : I copied this from the Bone-Util Addon by trzr 1051 #if bone.parent: 1052 # co.x, co.y, co.z = -co.y, co.z, co.x 1053 # rot.w, rot.x, rot.y, rot.z = rot.w, rot.y, -rot.z, -rot.x 1054 #else: 1055 # co.x, co.y, co.z = -co.x, co.z, -co.y 1056 # 1057 # rot = compat.mul(rot, mathutils.Quaternion((0, 0, 1), math.radians(90))) 1058 # rot.w, rot.x, rot.y, rot.z = -rot.w, -rot.x, rot.z, -rot.y 1059 1060 #opengl_mat = compat.convert_blend_z_up_to_opengl_y_up_mat4(bone.matrix) 1061 # 1062 #if bone.parent: 1063 # opengl_mat = compat.mul(compat.convert_blend_z_up_to_opengl_y_up_mat4(bone.parent.matrix).inverted(), opengl_mat) 1064 # 1065 #co = opengl_mat.to_translation() * self.scale 1066 #rot = opengl_mat.to_quaternion() 1067 1068 data = { 1069 'name': common.encode_bone_name(bone.name, self.is_convert_bone_weight_names), 1070 'scl': is_scl_bone, 1071 'parent_index': parent_index, 1072 'co': co.copy(), 1073 'rot': rot.copy(), 1074 } 1075 scale = arm.edit_bones[bone.name].get('cm3d2_bone_scale') 1076 if scale: 1077 data['scale'] = scale 1078 bone_data.append(data) 1079 1080 bpy.ops.object.mode_set(mode=pre_mode) 1081 compat.set_active(context, pre_active) 1082 return bone_data 1083 1084 @staticmethod 1085 def bone_data_parser(container): 1086 """BoneData テキストをパースして辞書を要素とするリストを返す""" 1087 bone_data = [] 1088 bone_name_indices = {} 1089 for line in container: 1090 data = line.split(',') 1091 if len(data) < 5: 1092 continue 1093 1094 parent_name = data[2] 1095 if parent_name.isdigit(): 1096 parent_index = int(parent_name) 1097 else: 1098 parent_index = bone_name_indices.get(parent_name, -1) 1099 1100 bone_datum = { 1101 'name': data[0], 1102 'scl': int(data[1]), 1103 'parent_index': parent_index, 1104 'co': list(map(float, data[3].split())), 1105 'rot': list(map(float, data[4].split())), 1106 } 1107 # scale info (for version 2001 or later) 1108 if len(data) >= 7: 1109 if data[5] == '1': 1110 bone_scale = data[6] 1111 bone_datum['scale'] = list(map(float, bone_scale.split())) 1112 bone_data.append(bone_datum) 1113 bone_name_indices[data[0]] = len(bone_name_indices) 1114 return bone_data 1115 1116 def armature_local_bone_data_parser(self, ob): 1117 """アーマチュアを解析してBoneDataを返す""" 1118 arm = ob.data 1119 1120 # XXX Instead of just adding all bones, only bones / bones-with-decendants 1121 # that have use_deform == True or mathcing vertex groups should be used 1122 bones = [] 1123 bone_name_indices = {} 1124 already_bone_names = [] 1125 bones_queue = arm.bones[:] 1126 while len(bones_queue): 1127 bone = bones_queue.pop(0) 1128 1129 if not bone.parent: 1130 already_bone_names.append(bone.name) 1131 bones.append(bone) 1132 bone_name_indices[bone.name] = len(bone_name_indices) 1133 continue 1134 elif bone.parent.name in already_bone_names: 1135 already_bone_names.append(bone.name) 1136 bones.append(bone) 1137 bone_name_indices[bone.name] = len(bone_name_indices) 1138 continue 1139 1140 bones_queue.append(bone) 1141 1142 local_bone_data = [] 1143 for bone in bones: 1144 mat = bone.matrix_local.copy() 1145 mat = compat.mul(mathutils.Matrix.Scale(-1, 4, (1, 0, 0)), mat) 1146 mat = compat.convert_bl_to_cm_bone_rotation(mat) 1147 pos = mat.translation.copy() 1148 1149 mat.transpose() 1150 mat.row[3] = (0.0, 0.0, 0.0, 1.0) 1151 pos = compat.mul(mat.to_3x3(), pos) 1152 pos *= -self.scale 1153 mat.translation = pos 1154 mat.transpose() 1155 1156 #co = mat.to_translation() * self.scale 1157 #rot = mat.to_quaternion() 1158 # 1159 #co.rotate(rot.inverted()) 1160 #co.x, co.y, co.z = co.y, co.x, -co.z 1161 # 1162 #fix_quat = mathutils.Euler((0, 0, math.radians(-90)), 'XYZ').to_quaternion() 1163 #rot = compat.mul(rot, fix_quat) 1164 #rot.w, rot.x, rot.y, rot.z = -rot.z, -rot.y, -rot.x, rot.w 1165 # 1166 #co_mat = mathutils.Matrix.Translation(co) 1167 #rot_mat = rot.to_matrix().to_4x4() 1168 #mat = compat.mul(co_mat, rot_mat) 1169 # 1170 #copy_mat = mat.copy() 1171 #mat[0][0], mat[0][1], mat[0][2], mat[0][3] = copy_mat[0][0], copy_mat[1][0], copy_mat[2][0], copy_mat[3][0] 1172 #mat[1][0], mat[1][1], mat[1][2], mat[1][3] = copy_mat[0][1], copy_mat[1][1], copy_mat[2][1], copy_mat[3][1] 1173 #mat[2][0], mat[2][1], mat[2][2], mat[2][3] = copy_mat[0][2], copy_mat[1][2], copy_mat[2][2], copy_mat[3][2] 1174 #mat[3][0], mat[3][1], mat[3][2], mat[3][3] = copy_mat[0][3], copy_mat[1][3], copy_mat[2][3], copy_mat[3][3] 1175 1176 mat_array = [] 1177 for vec in mat: 1178 mat_array.extend(vec[:]) 1179 1180 local_bone_data.append({ 1181 'name': common.encode_bone_name(bone.name, self.is_convert_bone_weight_names), 1182 'matrix': mat_array, 1183 }) 1184 return local_bone_data 1185 1186 @staticmethod 1187 def local_bone_data_parser(container): 1188 """LocalBoneData テキストをパースして辞書を要素とするリストを返す""" 1189 local_bone_data = [] 1190 for line in container: 1191 data = line.split(',') 1192 if len(data) != 2: 1193 continue 1194 local_bone_data.append({ 1195 'name': data[0], 1196 'matrix': list(map(float, data[1].split())), 1197 }) 1198 return local_bone_data 1199 1200 @staticmethod 1201 def indexed_data_generator(container, prefix='', max_index=9**9, max_pass=50): 1202 """コンテナ内の数値インデックスをキーに持つ要素を昇順に返すジェネレーター""" 1203 pass_count = 0 1204 for i in range(max_index): 1205 name = prefix + str(i) 1206 if name not in container: 1207 pass_count += 1 1208 if max_pass < pass_count: 1209 return 1210 continue 1211 yield container[name] 1212 1213 1214# メニューを登録する関数 1215def menu_func(self, context): 1216 self.layout.operator(CNV_OT_export_cm3d2_model.bl_idname, icon_value=common.kiss_icon())
@compat.BlRegister()
class
CNV_OT_export_cm3d2_model17@compat.BlRegister() 18class CNV_OT_export_cm3d2_model(bpy.types.Operator): 19 bl_idname = 'export_mesh.export_cm3d2_model' 20 bl_label = "CM3D2モデル (.model)" 21 bl_description = "カスタムメイド3D2のmodelファイルを書き出します" 22 bl_options = {'REGISTER'} 23 24 filepath = bpy.props.StringProperty(subtype='FILE_PATH') 25 filename_ext = ".model" 26 filter_glob = bpy.props.StringProperty(default="*.model", options={'HIDDEN'}) 27 28 scale = bpy.props.FloatProperty(name="倍率", default=0.2, min=0.01, max=100, soft_min=0.01, soft_max=100, step=10, precision=2, description="エクスポート時のメッシュ等の拡大率です") 29 30 is_backup = bpy.props.BoolProperty(name="ファイルをバックアップ", default=True, description="ファイルに上書きする場合にバックアップファイルを複製します") 31 32 version = bpy.props.EnumProperty( 33 name="ファイルバージョン", 34 items=[ 35 ('2001', '2001', 'model version 2001 (available only for com3d2)', 'NONE', 0), 36 ('2000', '2000', 'model version 2000 (com3d2 version)', 'NONE', 1), 37 ('1000', '1000', 'model version 1000 (available for cm3d2/com3d2)', 'NONE', 2), 38 ], default='1000') 39 model_name = bpy.props.StringProperty(name="model名", default="*") 40 base_bone_name = bpy.props.StringProperty(name="基点ボーン名", default="*") 41 42 items = [ 43 ('ARMATURE' , "アーマチュア", "", 'OUTLINER_OB_ARMATURE', 1), 44 ('TEXT' , "テキスト", "", 'FILE_TEXT', 2), 45 ('OBJECT_PROPERTY' , "オブジェクト内プロパティ", "", 'OBJECT_DATAMODE', 3), 46 ('ARMATURE_PROPERTY', "アーマチュア内プロパティ", "", 'ARMATURE_DATA', 4), 47 ] 48 bone_info_mode = bpy.props.EnumProperty(items=items, name="ボーン情報元", default='OBJECT_PROPERTY', description="modelファイルに必要なボーン情報をどこから引っ張ってくるか選びます") 49 50 items = [ 51 ('TEXT', "テキスト", "", 'FILE_TEXT', 1), 52 ('MATERIAL', "マテリアル", "", 'MATERIAL', 2), 53 ] 54 mate_info_mode = bpy.props.EnumProperty(items=items, name="マテリアル情報元", default='MATERIAL', description="modelファイルに必要なマテリアル情報をどこから引っ張ってくるか選びます") 55 56 is_arrange_name = bpy.props.BoolProperty(name="データ名の連番を削除", default=True, description="「○○.001」のような連番が付属したデータ名からこれらを削除します") 57 58 is_align_to_base_bone = bpy.props.BoolProperty(name="Align to Base Bone", default=True, description="Align the object to it's base bone") 59 is_convert_tris = bpy.props.BoolProperty(name="四角面を三角面に", default=True, description="四角ポリゴンを三角ポリゴンに変換してから出力します、元のメッシュには影響ありません") 60 is_split_sharp = bpy.props.BoolProperty(name="Split Sharp Edges", default=True, description="Split all edges marked as sharp.") 61 is_normalize_weight = bpy.props.BoolProperty(name="ウェイトの合計を1.0に", default=True, description="4つのウェイトの合計値が1.0になるように正規化します") 62 is_convert_bone_weight_names = bpy.props.BoolProperty(name="頂点グループ名をCM3D2用に変換", default=True, description="全ての頂点グループ名をCM3D2で使える名前にしてからエクスポートします") 63 is_clean_vertex_groups = bpy.props.BoolProperty(name="クリーンな頂点グループ", default=True, description="重みがゼロの場合、頂点グループから頂点を削除します") 64 65 is_batch = bpy.props.BoolProperty(name="バッチモード", default=False, description="モードの切替やエラー個所の選択を行いません") 66 67 export_tangent = bpy.props.BoolProperty(name="接空間情報出力", default=False, description="接空間情報(binormals, tangents)を出力する") 68 69 70 shapekey_threshold = bpy.props.FloatProperty(name="Shape Key Threshold", default=0.00100, min=0, soft_min=0.0005, max=0.01, soft_max=0.002, precision=5, description="Lower values increase accuracy and file size. Higher values truncate small changes and reduce file size.") 71 export_shapekey_normals = bpy.props.BoolProperty(name="Export Shape Key Normals", default=True, description="Export custom normals for each shape key on export.") 72 shapekey_normals_blend = bpy.props.FloatProperty(name="Shape Key Normals Blend", default=0.6, min=0, max=1, precision=3, description="Adjust the influence of shape keys on custom normals") 73 use_shapekey_colors = bpy.props.BoolProperty(name="Use Shape Key Colors", default=True, description="Use the shape key normals stored in the vertex colors instead of calculating the normals on export. (Recommend disabling if geometry was customized)") 74 75 76 @classmethod 77 def poll(cls, context): 78 ob = context.active_object 79 if ob: 80 if ob.type == 'MESH': 81 return True 82 return False 83 84 def report_cancel(self, report_message, report_type={'ERROR'}, resobj={'CANCELLED'}): 85 """エラーメッセージを出力してキャンセルオブジェクトを返す""" 86 self.report(type=report_type, message=report_message) 87 return resobj 88 89 def precheck(self, context): 90 """データの成否チェック""" 91 ob = context.active_object 92 if not ob: 93 return self.report_cancel("アクティブオブジェクトがありません") 94 if ob.type != 'MESH': 95 return self.report_cancel("メッシュオブジェクトを選択した状態で実行してください") 96 if not len(ob.material_slots): 97 return self.report_cancel("マテリアルがありません") 98 for slot in ob.material_slots: 99 if not slot.material: 100 return self.report_cancel("空のマテリアルスロットを削除してください") 101 try: 102 slot.material['shader1'] 103 slot.material['shader2'] 104 except: 105 return self.report_cancel("マテリアルに「shader1」と「shader2」という名前のカスタムプロパティを用意してください") 106 me = ob.data 107 if not me.uv_layers.active: 108 return self.report_cancel("UVがありません") 109 if 65535 < len(me.vertices): 110 return self.report_cancel("エクスポート可能な頂点数を大幅に超えています、最低でも65535未満には削減してください") 111 return None 112 113 def invoke(self, context, event): 114 res = self.precheck(context) 115 if res: 116 return res 117 ob = context.active_object 118 119 # model名とか 120 ob_names = common.remove_serial_number(ob.name, self.is_arrange_name).split('.') 121 self.model_name = ob_names[0] 122 self.base_bone_name = ob_names[1] if 2 <= len(ob_names) else 'Auto' 123 124 # ボーン情報元のデフォルトオプションを取得 125 arm_ob = ob.parent 126 for mod in ob.modifiers: 127 if mod.type == 'ARMATURE' and mod.object: 128 arm_ob = mod.object 129 if arm_ob and not arm_ob.type == 'ARMATURE': 130 arm_ob = None 131 132 info_mode_was_armature = (self.bone_info_mode == 'ARMATURE') 133 if "BoneData" in context.blend_data.texts: 134 if "LocalBoneData" in context.blend_data.texts: 135 self.bone_info_mode = 'TEXT' 136 if "BoneData:0" in ob: 137 ver = ob.get("ModelVersion") 138 if ver and ver >= 1000: 139 self.version = str(ver) 140 if "LocalBoneData:0" in ob: 141 self.bone_info_mode = 'OBJECT_PROPERTY' 142 if arm_ob: 143 if info_mode_was_armature: 144 self.bone_info_mode = 'ARMATURE' 145 else: 146 self.bone_info_mode = 'ARMATURE_PROPERTY' 147 148 # エクスポート時のデフォルトパスを取得 149 #if not self.filepath[-6:] == '.model': 150 if common.preferences().model_default_path: 151 self.filepath = common.default_cm3d2_dir(common.preferences().model_default_path, self.model_name, "model") 152 else: 153 self.filepath = common.default_cm3d2_dir(common.preferences().model_export_path, self.model_name, "model") 154 155 # バックアップ関係 156 self.is_backup = bool(common.preferences().backup_ext) 157 158 self.scale = 1.0 / common.preferences().scale 159 context.window_manager.fileselect_add(self) 160 return {'RUNNING_MODAL'} 161 162 # 'is_batch' がオンなら非表示 163 def draw(self, context): 164 self.layout.prop(self, 'scale') 165 row = self.layout.row() 166 row.prop(self, 'is_backup', icon='FILE_BACKUP') 167 if not common.preferences().backup_ext: 168 row.enabled = False 169 self.layout.prop(self, 'is_arrange_name', icon='FILE_TICK') 170 box = self.layout.box() 171 box.prop(self, 'version', icon='LINENUMBERS_ON') 172 box.prop(self, 'model_name', icon='SORTALPHA') 173 174 row = box.row() 175 row.prop(self, 'base_bone_name', icon='CONSTRAINT_BONE') 176 if self.base_bone_name == 'Auto': 177 row.enabled = False 178 179 prefs = common.preferences() 180 181 box = self.layout.box() 182 col = box.column(align=True) 183 col.label(text="ボーン情報元", icon='BONE_DATA') 184 col.prop(self, 'bone_info_mode', icon='BONE_DATA', expand=True) 185 col = box.column(align=True) 186 col.label(text="マテリアル情報元", icon='MATERIAL') 187 col.prop(self, 'mate_info_mode', icon='MATERIAL', expand=True) 188 189 box = self.layout.box() 190 box.label(text="メッシュオプション") 191 box.prop(self , 'is_align_to_base_bone', icon=compat.icon('OBJECT_ORIGIN' )) 192 box.prop(self , 'is_convert_tris' , icon=compat.icon('MESH_DATA' )) 193 box.prop(self , 'is_split_sharp' , icon=compat.icon('MOD_EDGESPLIT' )) 194 box.prop(self , 'export_tangent' , icon=compat.icon('CURVE_BEZCIRCLE')) 195 sub_box = box.box() 196 sub_box.prop(self , 'shapekey_threshold' , icon=compat.icon('SHAPEKEY_DATA' ), slider=True) 197 sub_box.prop(prefs, 'skip_shapekey' , icon=compat.icon('SHAPEKEY_DATA' ), toggle=1) 198 sub_box.prop(self , 'export_shapekey_normals', icon=compat.icon('NORMALS_VERTEX_FACE')) 199 row = sub_box.row() 200 row .prop(self , 'shapekey_normals_blend' , icon=compat.icon('MOD_NORMALEDIT' ), slider=True) 201 row.enabled = self.export_shapekey_normals 202 row = sub_box.row() 203 row .prop(self , 'use_shapekey_colors' , icon=compat.icon('GROUP_VCOL') , toggle=0) 204 row.enabled = self.export_shapekey_normals 205 sub_box = box.box() 206 sub_box.prop(self, 'is_normalize_weight', icon='MOD_VERTEX_WEIGHT') 207 sub_box.prop(self, 'is_clean_vertex_groups', icon='MOD_VERTEX_WEIGHT') 208 sub_box.prop(self, 'is_convert_bone_weight_names', icon_value=common.kiss_icon()) 209 sub_box 210 sub_box = box.box() 211 sub_box.prop(prefs, 'is_apply_modifiers', icon='MODIFIER') 212 row = sub_box.row() 213 row.prop(prefs, 'custom_normal_blend', icon='SNAP_NORMAL', slider=True) 214 row.enabled = prefs.is_apply_modifiers 215 216 def copy_and_activate_ob(self, context, ob): 217 new_ob = ob.copy() 218 new_me = ob.data.copy() 219 new_ob.data = new_me 220 compat.link(context.scene, new_ob) 221 compat.set_active(context, new_ob) 222 compat.set_select(new_ob, True) 223 return new_ob 224 225 def execute(self, context): 226 start_time = time.time() 227 prefs = common.preferences() 228 229 selected_objs = context.selected_objects 230 source_objs = [] 231 prev_mode = None 232 try: 233 ob_source = context.active_object 234 if ob_source not in selected_objs: 235 selected_objs.append(ob_source) # luvoid : Fix error where object is active but not selected 236 ob_name = ob_source.name 237 ob_main = None 238 if self.is_batch: 239 # アクティブオブジェクトを1つコピーするだけでjoinしない 240 source_objs.append(ob_source) 241 compat.set_select(ob_source, False) 242 ob_main = self.copy_and_activate_ob(context, ob_source) 243 244 if prefs.is_apply_modifiers and bpy.ops.object.forced_modifier_apply.poll(context): 245 bpy.ops.object.forced_modifier_apply(is_applies=[True for i in range(32)]) 246 else: 247 selected_count = 0 248 # 選択されたMESHオブジェクトをコピーしてjoin 249 # 必要に応じて、モディファイアの強制適用を行う 250 for selected in selected_objs: 251 source_objs.append(selected) 252 253 compat.set_select(selected, False) 254 255 if selected.type == 'MESH': 256 ob_created = self.copy_and_activate_ob(context, selected) 257 if selected == ob_source: 258 ob_main = ob_created 259 if prefs.is_apply_modifiers: 260 bpy.ops.object.forced_modifier_apply(apply_viewport_visible=True) 261 262 selected_count += 1 263 264 mode = context.active_object.mode 265 if mode != 'OBJECT': 266 prev_mode = mode 267 bpy.ops.object.mode_set(mode='OBJECT') 268 269 if selected_count > 1: 270 if ob_main: 271 compat.set_active(context, ob_main) 272 bpy.ops.object.join() 273 self.report(type={'INFO'}, message=f_tip_("{}個のオブジェクトをマージしました", selected_count)) 274 275 ret = self.export(context, ob_main) 276 if 'FINISHED' not in ret: 277 return ret 278 279 context.window_manager.progress_update(10) 280 diff_time = time.time() - start_time 281 self.report(type={'INFO'}, message=f_tip_("modelのエクスポートが完了しました。{:.2f} 秒 file={}", diff_time, self.filepath)) 282 return ret 283 finally: 284 # 作業データの破棄(コピーデータを削除、選択状態の復元、アクティブオブジェクト、モードの復元) 285 if ob_main: 286 common.remove_data(ob_main) 287 # me_copied = ob_main.data 288 # context.blend_data.objects.remove(ob_main, do_unlink=True) 289 # context.blend_data.meshes.remove(me_copied, do_unlink=True) 290 291 for obj in source_objs: 292 compat.set_select(obj, True) 293 294 if ob_source: 295 # TODO 元のオブジェクトをアクティブに戻す 296 if ob_name in bpy.data.objects: 297 compat.set_active(context, ob_source) 298 299 if prev_mode: 300 bpy.ops.object.mode_set(mode=prev_mode) 301 302 def export(self, context, ob): 303 """モデルファイルを出力""" 304 prefs = common.preferences() 305 306 if not self.is_batch: 307 prefs.model_export_path = self.filepath 308 prefs.scale = 1.0 / self.scale 309 310 context.window_manager.progress_begin(0, 10) 311 context.window_manager.progress_update(0) 312 313 res = self.precheck(context) 314 if res: 315 return res 316 me = ob.data 317 318 if ob.active_shape_key_index != 0: 319 ob.active_shape_key_index = 0 320 me.update() 321 322 # データの成否チェック 323 if self.bone_info_mode == 'ARMATURE': 324 arm_ob = ob.parent 325 if arm_ob and arm_ob.type != 'ARMATURE': 326 return self.report_cancel("メッシュオブジェクトの親がアーマチュアではありません") 327 if not arm_ob: 328 try: 329 arm_ob = next(mod for mod in ob.modifiers if mod.type == 'ARMATURE' and mod.object) 330 except StopIteration: 331 return self.report_cancel("アーマチュアが見つかりません、親にするかモディファイアにして下さい") 332 arm_ob = arm_ob.object 333 elif self.bone_info_mode == 'TEXT': 334 if "BoneData" not in context.blend_data.texts: 335 return self.report_cancel("テキスト「BoneData」が見つかりません、中止します") 336 if "LocalBoneData" not in context.blend_data.texts: 337 return self.report_cancel("テキスト「LocalBoneData」が見つかりません、中止します") 338 elif self.bone_info_mode == 'OBJECT_PROPERTY': 339 if "BoneData:0" not in ob: 340 return self.report_cancel("オブジェクトのカスタムプロパティにボーン情報がありません") 341 if "LocalBoneData:0" not in ob: 342 return self.report_cancel("オブジェクトのカスタムプロパティにボーン情報がありません") 343 elif self.bone_info_mode == 'ARMATURE_PROPERTY': 344 arm_ob = ob.parent 345 if arm_ob and arm_ob.type != 'ARMATURE': 346 return self.report_cancel("メッシュオブジェクトの親がアーマチュアではありません") 347 if not arm_ob: 348 try: 349 arm_ob = next(mod for mod in ob.modifiers if mod.type == 'ARMATURE' and mod.object) 350 except StopIteration: 351 return self.report_cancel("アーマチュアが見つかりません、親にするかモディファイアにして下さい") 352 arm_ob = arm_ob.object 353 if "BoneData:0" not in arm_ob.data: 354 return self.report_cancel("アーマチュアのカスタムプロパティにボーン情報がありません") 355 if "LocalBoneData:0" not in arm_ob.data: 356 return self.report_cancel("アーマチュアのカスタムプロパティにボーン情報がありません") 357 else: 358 return self.report_cancel("ボーン情報元のモードがおかしいです") 359 360 if self.mate_info_mode == 'TEXT': 361 for index, slot in enumerate(ob.material_slots): 362 if "Material:" + str(index) not in context.blend_data.texts: 363 return self.report_cancel("マテリアル情報元のテキストが足りません") 364 context.window_manager.progress_update(1) 365 366 # model名とか 367 ob_names = common.remove_serial_number(ob.name, self.is_arrange_name).split('.') 368 if self.model_name == '*': 369 self.model_name = ob_names[0] 370 if self.base_bone_name == '*': 371 self.base_bone_name = ob_names[1] if 2 <= len(ob_names) else 'Auto' 372 373 # BoneData情報読み込み 374 base_bone_candidate = None 375 bone_data = [] 376 if self.bone_info_mode == 'ARMATURE': 377 bone_data = self.armature_bone_data_parser(context, arm_ob) 378 base_bone_candidate = arm_ob.data['BaseBone'] 379 elif self.bone_info_mode == 'TEXT': 380 bone_data_text = context.blend_data.texts["BoneData"] 381 if 'BaseBone' in bone_data_text: 382 base_bone_candidate = bone_data_text['BaseBone'] 383 bone_data = self.bone_data_parser(l.body for l in bone_data_text.lines) 384 elif self.bone_info_mode in ['OBJECT_PROPERTY', 'ARMATURE_PROPERTY']: 385 target = ob if self.bone_info_mode == 'OBJECT_PROPERTY' else arm_ob.data 386 if 'BaseBone' in target: 387 base_bone_candidate = target['BaseBone'] 388 bone_data = self.bone_data_parser(self.indexed_data_generator(target, prefix="BoneData:")) 389 if len(bone_data) <= 0: 390 return self.report_cancel("テキスト「BoneData」に有効なデータがありません") 391 392 if self.base_bone_name not in (b['name'] for b in bone_data): 393 if base_bone_candidate and self.base_bone_name == 'Auto': 394 self.base_bone_name = base_bone_candidate 395 else: 396 return self.report_cancel("基点ボーンが存在しません") 397 bone_name_indices = {bone['name']: index for index, bone in enumerate(bone_data)} 398 context.window_manager.progress_update(2) 399 400 if self.is_align_to_base_bone: 401 bpy.ops.object.align_to_cm3d2_base_bone(scale=1.0/self.scale, is_preserve_mesh=True, bone_info_mode=self.bone_info_mode) 402 me.update() 403 404 if self.is_split_sharp: 405 bpy.ops.object.mode_set(mode='EDIT') 406 bpy.ops.mesh.split_sharp() 407 bpy.ops.object.mode_set(mode='OBJECT') 408 409 # LocalBoneData情報読み込み 410 local_bone_data = [] 411 if self.bone_info_mode == 'ARMATURE': 412 local_bone_data = self.armature_local_bone_data_parser(arm_ob) 413 elif self.bone_info_mode == 'TEXT': 414 local_bone_data_text = context.blend_data.texts["LocalBoneData"] 415 local_bone_data = self.local_bone_data_parser(l.body for l in local_bone_data_text.lines) 416 elif self.bone_info_mode in ['OBJECT_PROPERTY', 'ARMATURE_PROPERTY']: 417 target = ob if self.bone_info_mode == 'OBJECT_PROPERTY' else arm_ob.data 418 local_bone_data = self.local_bone_data_parser(self.indexed_data_generator(target, prefix="LocalBoneData:")) 419 if len(local_bone_data) <= 0: 420 return self.report_cancel("テキスト「LocalBoneData」に有効なデータがありません") 421 local_bone_name_indices = {bone['name']: index for index, bone in enumerate(local_bone_data)} 422 context.window_manager.progress_update(3) 423 424 used_local_bone = {index: False for index, bone in enumerate(local_bone_data)} 425 426 # ウェイト情報読み込み 427 vertices = [] 428 is_over_one = 0 429 is_under_one = 0 430 is_in_too_many = 0 431 for i, vert in enumerate(me.vertices): 432 vgs = [] 433 for vg in vert.groups: 434 if len(ob.vertex_groups) <= vg.group: # Apparently a vertex can be assigned to a non-existent group. 435 continue 436 name = common.encode_bone_name(ob.vertex_groups[vg.group].name, self.is_convert_bone_weight_names) 437 index = local_bone_name_indices.get(name, -1) 438 if index >= 0 and (vg.weight > 0.0 or not self.is_clean_vertex_groups): 439 vgs.append([index, vg.weight]) 440 # luvoid : track used bones 441 used_local_bone[index] = True 442 boneindex = bone_name_indices.get(name, -1) 443 while boneindex >= 0: 444 parent = bone_data[boneindex] 445 localindex = local_bone_name_indices.get(parent['name'], -1) 446 # could check for `localindex == -1` here, but it's prescence may be useful in determing if the local bones resolve back to some root 447 used_local_bone[localindex] = True 448 boneindex = parent['parent_index'] 449 if len(vgs) == 0: 450 if not self.is_batch: 451 self.select_no_weight_vertices(context, local_bone_name_indices) 452 return self.report_cancel("ウェイトが割り当てられていない頂点が見つかりました、中止します") 453 if len(vgs) > 4: 454 is_in_too_many += 1 455 vgs = sorted(vgs, key=itemgetter(1), reverse=True)[0:4] 456 total = sum(vg[1] for vg in vgs) 457 if self.is_normalize_weight: 458 for vg in vgs: 459 vg[1] /= total 460 else: 461 if 1.01 < total: 462 is_over_one += 1 463 elif total < 0.99: 464 is_under_one += 1 465 if len(vgs) < 4: 466 vgs += [(0, 0.0)] * (4 - len(vgs)) 467 vertices.append({ 468 'index': vert.index, 469 'face_indexs': list(map(itemgetter(0), vgs)), 470 'weights': list(map(itemgetter(1), vgs)), 471 }) 472 473 if 1 <= is_over_one: 474 self.report(type={'WARNING'}, message=f_tip_("ウェイトの合計が1.0を超えている頂点が見つかりました。正規化してください。超過している頂点の数:{}", is_over_one)) 475 if 1 <= is_under_one: 476 self.report(type={'WARNING'}, message=f_tip_("ウェイトの合計が1.0未満の頂点が見つかりました。正規化してください。不足している頂点の数:{}", is_under_one)) 477 478 # luvoid : warn that there are vertices in too many vertex groups 479 if is_in_too_many > 0: 480 self.report(type={'WARNING'}, message=f_tip_("4つを超える頂点グループにある頂点が見つかりました。頂点グループをクリーンアップしてください。不足している頂点の数:{}", is_in_too_many)) 481 482 # luvoid : check for unused local bones that the game will delete 483 is_deleted = 0 484 deleted_names = "The game will delete these local bones" 485 for index, is_used in used_local_bone.items(): 486 print(index, is_used) 487 if is_used == False: 488 is_deleted += 1 489 deleted_names = deleted_names + '\n' + local_bone_data[index]['name'] 490 elif is_used == True: 491 pass 492 else: 493 print(f_tip_("Unexpected: used_local_bone[{key}] == {value} when len(used_local_bone) == {length}", key=index, value=is_used, length=len(used_local_bone))) 494 self.report(type={'WARNING'}, message=f_tip_("Could not find whether bone with index {index} was used. See console for more info.", index=i)) 495 if is_deleted > 0: 496 self.report(type={'WARNING'}, message=f_tip_("頂点が割り当てられていない{num}つのローカルボーンが見つかりました。 詳細については、ログを参照してください。", num=is_deleted)) 497 self.report(type={'INFO'}, message=deleted_names) 498 499 context.window_manager.progress_update(4) 500 501 502 try: 503 writer = common.open_temporary(self.filepath, 'wb', is_backup=self.is_backup) 504 except: 505 self.report(type={'ERROR'}, message=f_tip_("ファイルを開くのに失敗しました、アクセス不可かファイルが存在しません。file={}", self.filepath)) 506 return {'CANCELLED'} 507 508 model_datas = { 509 'bone_data': bone_data, 510 'local_bone_data': local_bone_data, 511 'vertices': vertices, 512 } 513 try: 514 with writer: 515 self.write_model(context, ob, writer, **model_datas) 516 except common.CM3D2ExportException as e: 517 self.report(type={'ERROR'}, message=str(e)) 518 return {'CANCELLED'} 519 520 return {'FINISHED'} 521 522 def write_model(self, context, ob, writer, bone_data=[], local_bone_data=[], vertices=[]): 523 """モデルデータをファイルオブジェクトに書き込む""" 524 me = ob.data 525 prefs = common.preferences() 526 527 # ファイル先頭 528 common.write_str(writer, 'CM3D2_MESH') 529 self.version_num = int(self.version) 530 writer.write(struct.pack('<i', self.version_num)) 531 532 common.write_str(writer, self.model_name) 533 common.write_str(writer, self.base_bone_name) 534 535 # ボーン情報書き出し 536 writer.write(struct.pack('<i', len(bone_data))) 537 for bone in bone_data: 538 common.write_str(writer, bone['name']) 539 writer.write(struct.pack('<b', bone['scl'])) 540 context.window_manager.progress_update(3.3) 541 for bone in bone_data: 542 writer.write(struct.pack('<i', bone['parent_index'])) 543 context.window_manager.progress_update(3.7) 544 for bone in bone_data: 545 writer.write(struct.pack('<3f', bone['co'][0], bone['co'][1], bone['co'][2])) 546 writer.write(struct.pack('<4f', bone['rot'][1], bone['rot'][2], bone['rot'][3], bone['rot'][0])) 547 if self.version_num >= 2001: 548 use_scale = ('scale' in bone) 549 writer.write(struct.pack('<b', use_scale)) 550 if use_scale: 551 bone_scale = bone['scale'] 552 writer.write(struct.pack('<3f', bone_scale[0], bone_scale[1], bone_scale[2])) 553 context.window_manager.progress_update(4) 554 555 # 正しい頂点数などを取得 556 bm = bmesh.new() 557 bm.from_mesh(me) 558 uv_lay = bm.loops.layers.uv.active 559 vert_uvs = [] 560 vert_uvs_append = vert_uvs.append 561 vert_iuv = {} 562 vert_indices = {} 563 vert_count = 0 564 for vert in bm.verts: 565 vert_uv = [] 566 vert_uvs_append(vert_uv) 567 for loop in vert.link_loops: 568 uv = loop[uv_lay].uv 569 if uv not in vert_uv: 570 vert_uv.append(uv) 571 vert_iuv[hash((vert.index, uv.x, uv.y))] = vert_count 572 vert_indices[vert.index] = vert_count 573 vert_count += 1 574 if 65535 < vert_count: 575 raise common.CM3D2ExportException(f_tip_("頂点数がまだ多いです (現在{}頂点)。あと{}頂点以上減らしてください、中止します", vert_count, vert_count - 65535)) 576 context.window_manager.progress_update(5) 577 578 writer.write(struct.pack('<2i', vert_count, len(ob.material_slots))) 579 580 # ローカルボーン情報を書き出し 581 writer.write(struct.pack('<i', len(local_bone_data))) 582 for bone in local_bone_data: 583 common.write_str(writer, bone['name']) 584 context.window_manager.progress_update(5.3) 585 for bone in local_bone_data: 586 for f in bone['matrix']: 587 writer.write(struct.pack('<f', f)) 588 context.window_manager.progress_update(5.7) 589 590 # カスタム法線情報を取得 591 if me.has_custom_normals: 592 custom_normals = [mathutils.Vector() for i in range(len(me.vertices))] 593 me.calc_normals_split() 594 for loop in me.loops: 595 custom_normals[loop.vertex_index] += loop.normal 596 for no in custom_normals: 597 no.normalize() 598 else: 599 custom_normals = None 600 601 cm_verts = [] 602 cm_norms = [] 603 cm_uvs = [] 604 # 頂点情報を書き出し 605 for i, vert in enumerate(bm.verts): 606 co = compat.convert_bl_to_cm_space( vert.co * self.scale ) 607 if me.has_custom_normals: 608 no = custom_normals[vert.index] 609 else: 610 no = vert.normal.copy() 611 no = compat.convert_bl_to_cm_space( no ) 612 for uv in vert_uvs[i]: 613 cm_verts.append(co) 614 cm_norms.append(no) 615 cm_uvs.append(uv) 616 writer.write(struct.pack('<3f', co.x, co.y, co.z)) 617 writer.write(struct.pack('<3f', no.x, no.y, no.z)) 618 writer.write(struct.pack('<2f', uv.x, uv.y)) 619 context.window_manager.progress_update(6) 620 621 cm_tris = self.parse_triangles(bm, ob, uv_lay, vert_iuv, vert_indices) 622 623 # 接空間情報を書き出し 624 if self.export_tangent: 625 tangents = self.calc_tangents(cm_tris, cm_verts, cm_norms, cm_uvs) 626 writer.write(struct.pack('<i', len(tangents))) 627 for t in tangents: 628 writer.write(struct.pack('<4f', *t)) 629 else: 630 writer.write(struct.pack('<i', 0)) 631 632 # ウェイト情報を書き出し 633 for vert in vertices: 634 for uv in vert_uvs[vert['index']]: 635 writer.write(struct.pack('<4H', *vert['face_indexs'])) 636 writer.write(struct.pack('<4f', *vert['weights'])) 637 context.window_manager.progress_update(7) 638 639 # 面情報を書き出し 640 for tri in cm_tris: 641 writer.write(struct.pack('<i', len(tri))) 642 for vert_index in tri: 643 writer.write(struct.pack('<H', vert_index)) 644 context.window_manager.progress_update(8) 645 646 # マテリアルを書き出し 647 writer.write(struct.pack('<i', len(ob.material_slots))) 648 for slot_index, slot in enumerate(ob.material_slots): 649 if self.mate_info_mode == 'MATERIAL': 650 mat_data = cm3d2_data.MaterialHandler.parse_mate(slot.material, self.is_arrange_name) 651 mat_data.write(writer, write_header=False) 652 653 elif self.mate_info_mode == 'TEXT': 654 text = context.blend_data.texts["Material:" + str(slot_index)].as_string() 655 mat_data = cm3d2_data.MaterialHandler.parse_text(slot.material, self.is_arrange_name) 656 mat_data.write(writer, write_header=False) 657 658 context.window_manager.progress_update(9) 659 660 # モーフを書き出し 661 if me.shape_keys and len(me.shape_keys.key_blocks) >= 2: 662 try: 663 self.write_shapekeys(context, ob, writer, vert_uvs, custom_normals) 664 finally: 665 print("FINISHED SHAPE KEYS WRITE") 666 pass 667 common.write_str(writer, 'end') 668 669 def write_shapekeys(self, context, ob, writer, vert_uvs, custom_normals=None): 670 # モーフを書き出し 671 me = ob.data 672 prefs = common.preferences() 673 674 is_use_attributes = (not compat.IS_LEGACY and bpy.app.version >= (2,92)) 675 676 loops_vert_index = np.empty((len(me.loops)), dtype=int) 677 me.loops.foreach_get('vertex_index', loops_vert_index.ravel()) 678 679 def find_normals_attribute(name) -> (bpy.types.Attribute, bool): 680 if is_use_attributes: 681 normals_color = me.attributes[name] if name in me.attributes.keys() else None 682 attribute_is_color = (not normals_color is None) and normals_color.data_type in {'BYTE_COLOR', 'FLOAT_COLOR'} 683 else: 684 normals_color = me.vertex_colors[name] if name in me.vertex_colors.keys() else None 685 attribute_is_color = True 686 return normals_color, attribute_is_color 687 688 if self.use_shapekey_colors: 689 static_attribute_colors = np.empty((len(me.loops), 4), dtype=float) 690 color_offset = np.array([[0.5,0.5,0.5]]) 691 loops_per_vertex = np.zeros((len(me.vertices))) 692 for loop in me.loops: 693 loops_per_vertex[loop.vertex_index] += 1 694 loops_per_vertex_reciprocal = np.reciprocal(loops_per_vertex, out=loops_per_vertex).reshape((len(me.vertices), 1)) 695 def get_sk_delta_normals_from_attribute(attribute, is_color, out): 696 if is_color: 697 attribute.data.foreach_get('color', static_attribute_colors.ravel()) 698 loop_delta_normals = static_attribute_colors[:,:3] 699 loop_delta_normals -= color_offset 700 loop_delta_normals *= 2 701 else: 702 loop_delta_normals = static_attribute_colors[:,:3] 703 attribute.data.foreach_get('vector', loop_delta_normals.ravel()) 704 705 vert_delta_normals = out 706 vert_delta_normals.fill(0) 707 708 # for loop in me.loops: vert_delta_normals[loop.vertex_index] += loop_delta_normals[loop.index] 709 np.add.at(vert_delta_normals, loops_vert_index, loop_delta_normals) # XXX Slower but handles edge cases better 710 #vert_delta_normals[loops_vert_index] += loop_delta_normals # XXX Only first loop's value will be kept 711 712 # for delta_normal in vert_delta_normals: delta_normal /= loops_per_vertex[vert.index] 713 vert_delta_normals *= loops_per_vertex_reciprocal 714 715 return out #.tolist() 716 717 if me.has_custom_normals: 718 basis_custom_normals = np.array(custom_normals, dtype=float) 719 static_loop_normals = np.empty((len(me.loops), 3), dtype=float) 720 static_vert_lengths = np.empty((len(me.vertices), 1), dtype=float) 721 def get_sk_delta_normals_from_custom_normals(shape_key, out): 722 vert_custom_normals = out 723 vert_custom_normals.fill(0) 724 725 loop_custom_normals = static_loop_normals 726 np.copyto(loop_custom_normals.ravel(), shape_key.normals_split_get()) 727 728 # for loop in me.loops: vert_delta_normals[loop.vertex_index] += loop_delta_normals[loop.index] 729 if not self.is_split_sharp: 730 # XXX Slower 731 np.add.at(vert_custom_normals, loops_vert_index, loop_custom_normals) 732 vert_len_sq = get_lengths_squared(vert_custom_normals, out=static_vert_lengths) 733 vert_len = np.sqrt(vert_len_sq, out=vert_len_sq) 734 np.reciprocal(vert_len, out=vert_len) 735 vert_custom_normals *= vert_len #.reshape((*vert_len.shape, 1)) 736 else: 737 # loop normals should be the same per-vertex unless there is a sharp edge 738 # or a flat shaded face, but all sharp edges were split, so this method is fine 739 # (and Flat shaded faces just won't be supported) 740 vert_custom_normals[loops_vert_index] += loop_custom_normals # Only first loop's value will be kept 741 742 vert_custom_normals -= basis_custom_normals 743 return out 744 745 if not me.has_custom_normals: 746 basis_normals = np.empty((len(me.vertices), 3), dtype=float) 747 me.vertices.foreach_get('normal', basis_normals.ravel()) 748 def get_sk_delta_normals_from_normals(shape_key, out): 749 vert_normals = out 750 np.copyto(vert_normals.ravel(), shape_key.normals_vertex_get()) 751 vert_delta_normals = np.subtract(vert_normals, basis_normals, out=out) 752 return out 753 754 basis_co = np.empty((len(me.vertices), 3), dtype=float) 755 me.vertices.foreach_get('co', basis_co.ravel()) 756 def get_sk_delta_coordinates(shape_key, out): 757 delta_coordinates = out 758 shape_key.data.foreach_get('co', delta_coordinates.ravel()) 759 delta_coordinates -= basis_co 760 return out 761 762 static_array_sq = np.empty((len(me.vertices), 3), dtype=float) 763 def get_lengths_squared(vectors, out): 764 np.power(vectors, 2, out=static_array_sq) 765 np.sum(static_array_sq, axis=1, out=out.ravel()) 766 return out 767 768 def write_morph(morph, name): 769 common.write_str(writer, 'morph') 770 common.write_str(writer, name) 771 writer.write(struct.pack('<i', len(morph))) 772 for v_index, vec, normal in morph: 773 vec = compat.convert_bl_to_cm_space(vec ) 774 normal = compat.convert_bl_to_cm_space(normal) 775 writer.write(struct.pack('<H', v_index)) 776 writer.write(struct.pack('<3f', *vec[:3])) 777 writer.write(struct.pack('<3f', *normal[:3])) 778 779 # accessing operator properties via "self.x" is SLOW, so store some here 780 self__export_shapekey_normals = self.export_shapekey_normals 781 self__use_shapekey_colors = self.use_shapekey_colors 782 self__shapekey_normals_blend = self.shapekey_normals_blend 783 self__scale = self.scale 784 785 co_diff_threshold = self.shapekey_threshold / 5 786 co_diff_threshold_squared = co_diff_threshold * co_diff_threshold 787 no_diff_threshold = self.shapekey_threshold * 10 788 no_diff_threshold_squared = no_diff_threshold * no_diff_threshold 789 790 # shared arrays 791 delta_coordinates = np.empty((len(me.vertices), 3), dtype=float) 792 vert_delta_normals = np.empty((len(me.vertices), 3), dtype=float) 793 loop_delta_normals = np.empty((len(me.loops ), 3), dtype=float) 794 795 delta_co_lensq = np.empty((len(me.vertices)), dtype=float) 796 delta_no_lensq = np.empty((len(me.vertices)), dtype=float) 797 798 if not self.export_shapekey_normals: 799 vert_delta_normals.fill(0) 800 delta_no_lensq.fill(0) 801 802 # HEAVY LOOP 803 for shape_key in me.shape_keys.key_blocks[1:]: 804 morph = [] 805 806 if self__export_shapekey_normals and self__use_shapekey_colors: 807 normals_color, attrubute_is_color = find_normals_attribute(f'{shape_key.name}_delta_normals') 808 809 if self__export_shapekey_normals: 810 if self__use_shapekey_colors and not normals_color is None: 811 sk_delta_normals = get_sk_delta_normals_from_attribute(normals_color, attrubute_is_color, out=vert_delta_normals) 812 elif me.has_custom_normals: 813 sk_delta_normals = get_sk_delta_normals_from_custom_normals(shape_key, out=vert_delta_normals) 814 sk_delta_normals *= self__shapekey_normals_blend 815 else: 816 sk_delta_normals = get_sk_delta_normals_from_normals(shape_key, out=vert_delta_normals) 817 sk_delta_normals *= self__shapekey_normals_blend 818 819 sk_no_lensq = get_lengths_squared(sk_delta_normals, out=delta_no_lensq) 820 else: 821 sk_delta_normals = vert_delta_normals 822 sk_no_lensq = delta_no_lensq 823 824 sk_co_diffs = get_sk_delta_coordinates(shape_key, out=delta_coordinates) 825 sk_co_diffs *= self__scale # scale before getting lengths 826 sk_co_lensq = get_lengths_squared(sk_co_diffs, out=delta_co_lensq) 827 828 # SUPER HEAVY LOOP 829 outvert_index = 0 830 for i in range(len(me.vertices)): 831 if sk_co_lensq[i] >= co_diff_threshold_squared or sk_no_lensq[i] >= no_diff_threshold_squared: 832 morph += [ (outvert_index+j, sk_co_diffs[i], sk_delta_normals[i]) for j in range(len(vert_uvs[i])) ] 833 else: 834 # ignore because change is too small (greatly lowers file size) 835 pass 836 outvert_index += len(vert_uvs[i]) 837 838 if prefs.skip_shapekey and not len(morph): 839 continue 840 else: 841 write_morph(morph, shape_key.name) 842 843 def write_tangents(self, writer, me): 844 if len(me.uv_layers) < 1: 845 return 846 847 num_loops = len(me.loops) 848 849 def parse_triangles(self, bm, ob, uv_lay, vert_iuv, vert_indices): 850 def vert_index_from_loops(loops): 851 """vert_index generator""" 852 for loop in loops: 853 uv = loop[uv_lay].uv 854 v_index = loop.vert.index 855 vert_index = vert_iuv.get(hash((v_index, uv.x, uv.y))) 856 if vert_index is None: 857 vert_index = vert_indices.get(v_index, 0) 858 yield vert_index 859 860 triangles = [] 861 for mate_index, slot in enumerate(ob.material_slots): 862 tris_faces = [] 863 for face in bm.faces: 864 if face.material_index != mate_index: 865 continue 866 if len(face.verts) == 3: 867 tris_faces.extend(vert_index_from_loops(reversed(face.loops))) 868 elif len(face.verts) == 4 and self.is_convert_tris: 869 v1 = face.loops[0].vert.co - face.loops[2].vert.co 870 v2 = face.loops[1].vert.co - face.loops[3].vert.co 871 if v1.length < v2.length: 872 f1 = [0, 1, 2] 873 f2 = [0, 2, 3] 874 else: 875 f1 = [0, 1, 3] 876 f2 = [1, 2, 3] 877 faces, faces2 = [], [] 878 for i, vert_index in enumerate(vert_index_from_loops(reversed(face.loops))): 879 if i in f1: 880 faces.append(vert_index) 881 if i in f2: 882 faces2.append(vert_index) 883 tris_faces.extend(faces) 884 tris_faces.extend(faces2) 885 elif 5 <= len(face.verts) and self.is_convert_tris: 886 face_count = len(face.verts) - 2 887 888 tris = [] 889 seek_min, seek_max = 0, len(face.verts) - 1 890 for i in range(face_count): 891 if not i % 2: 892 tris.append([seek_min, seek_min + 1, seek_max]) 893 seek_min += 1 894 else: 895 tris.append([seek_min, seek_max - 1, seek_max]) 896 seek_max -= 1 897 898 tris_indexs = [[] for _ in range(len(tris))] 899 for i, vert_index in enumerate(vert_index_from_loops(reversed(face.loops))): 900 for tris_index, points in enumerate(tris): 901 if i in points: 902 tris_indexs[tris_index].append(vert_index) 903 904 tris_faces.extend(p for ps in tris_indexs for p in ps) 905 906 triangles.append(tris_faces) 907 return triangles 908 909 def calc_tangents(self, cm_tris, cm_verts, cm_norms, cm_uvs): 910 count = len(cm_verts) 911 tan1 = [None] * count 912 tan2 = [None] * count 913 for i in range(0, count): 914 tan1[i] = mathutils.Vector((0, 0, 0)) 915 tan2[i] = mathutils.Vector((0, 0, 0)) 916 917 for tris in cm_tris: 918 tri_len = len(tris) 919 tri_idx = 0 920 while tri_idx < tri_len: 921 i1, i2, i3 = tris[tri_idx], tris[tri_idx + 1], tris[tri_idx + 2] 922 v1, v2, v3 = cm_verts[i1], cm_verts[i2], cm_verts[i3] 923 w1, w2, w3 = cm_uvs[i1], cm_uvs[i2], cm_uvs[i3] 924 925 a1 = v2 - v1 926 a2 = v3 - v1 927 s1 = w2 - w1 928 s2 = w3 - w1 929 930 r_inverse = (s1.x * s2.y - s2.x * s1.y) 931 932 if r_inverse != 0: 933 # print("i1 = {i1} i2 = {i2} i3 = {i3}".format(i1=i1, i2=i2, i3=i3)) 934 # print("v1 = {v1} v2 = {v2} v3 = {v3}".format(v1=v1, v2=v2, v3=v3)) 935 # print("w1 = {w1} w2 = {w2} w3 = {w3}".format(w1=w1, w2=w2, w3=w3)) 936 937 # print("a1 = {a1} a2 = {a2}".format(a1=a1, a2=a2)) 938 # print("s1 = {s1} s2 = {s2}".format(s1=s1, s2=s2)) 939 940 # print("r_inverse = ({s1x} * {s2y} - {s2x} * {s1y}) = {r_inverse}".format(r_inverse=r_inverse, s1x=s1.x, s1y=s1.y, s2x=s2.x, s2y=s2.y)) 941 942 r = 1.0 / r_inverse 943 sdir = mathutils.Vector(((s2.y * a1.x - s1.y * a2.x) * r, (s2.y * a1.y - s1.y * a2.y) * r, (s2.y * a1.z - s1.y * a2.z) * r)) 944 tan1[i1] += sdir 945 tan1[i2] += sdir 946 tan1[i3] += sdir 947 948 tdir = mathutils.Vector(((s1.x * a2.x - s2.x * a1.x) * r, (s1.x * a2.y - s2.x * a1.y) * r, (s1.x * a2.z - s2.x * a1.z) * r)) 949 tan2[i1] += tdir 950 tan2[i2] += tdir 951 tan2[i3] += tdir 952 953 tri_idx += 3 954 955 tangents = [None] * count 956 for i in range(0, count): 957 n = cm_norms[i] 958 ti = tan1[i] 959 t = (ti - n * n.dot(ti)).normalized() 960 961 c = n.cross(ti) 962 val = c.dot(tan2[i]) 963 w = 1.0 if val < 0 else -1.0 964 tangents[i] = (-t.x, t.y, t.z, w) 965 966 return tangents 967 968 def select_no_weight_vertices(self, context, local_bone_name_indices): 969 """ウェイトが割り当てられていない頂点を選択する""" 970 ob = context.active_object 971 me = ob.data 972 bpy.ops.object.mode_set(mode='EDIT') 973 bpy.ops.mesh.select_all(action='SELECT') 974 #bpy.ops.object.mode_set(mode='OBJECT') 975 context.tool_settings.mesh_select_mode = (True, False, False) 976 for vert in me.vertices: 977 for vg in vert.groups: 978 if len(ob.vertex_groups) <= vg.group: # Apparently a vertex can be assigned to a non-existent group. 979 continue 980 name = common.encode_bone_name(ob.vertex_groups[vg.group].name, self.is_convert_bone_weight_names) 981 if name in local_bone_name_indices and 0.0 < vg.weight: 982 vert.select = False 983 break 984 bpy.ops.object.mode_set(mode='EDIT') 985 986 def armature_bone_data_parser(self, context, ob): 987 """アーマチュアを解析してBoneDataを返す""" 988 arm = ob.data 989 990 pre_active = compat.get_active(context) 991 pre_mode = ob.mode 992 993 compat.set_active(context, ob) 994 bpy.ops.object.mode_set(mode='EDIT') 995 996 bones = [] 997 bone_name_indices = {} 998 already_bone_names = [] 999 bones_queue = arm.edit_bones[:] 1000 while len(bones_queue): 1001 bone = bones_queue.pop(0) 1002 1003 if not bone.parent: 1004 already_bone_names.append(bone.name) 1005 bones.append(bone) 1006 bone_name_indices[bone.name] = len(bone_name_indices) 1007 continue 1008 elif bone.parent.name in already_bone_names: 1009 already_bone_names.append(bone.name) 1010 bones.append(bone) 1011 bone_name_indices[bone.name] = len(bone_name_indices) 1012 continue 1013 1014 bones_queue.append(bone) 1015 1016 bone_data = [] 1017 for bone in bones: 1018 1019 # Also check for UnknownFlag for backwards compatibility 1020 is_scl_bone = bone['cm3d2_scl_bone'] if 'cm3d2_scl_bone' in bone \ 1021 else bone['UnknownFlag'] if 'UnknownFlag' in bone \ 1022 else 0 1023 parent_index = bone_name_indices[bone.parent.name] if bone.parent else -1 1024 1025 mat = bone.matrix.copy() 1026 1027 if bone.parent: 1028 mat = compat.convert_bl_to_cm_bone_rotation(mat) 1029 mat = compat.mul(bone.parent.matrix.inverted(), mat) 1030 mat = compat.convert_bl_to_cm_bone_space(mat) 1031 else: 1032 mat = compat.convert_bl_to_cm_bone_rotation(mat) 1033 mat = compat.convert_bl_to_cm_space(mat) 1034 1035 co = mat.to_translation() * self.scale 1036 rot = mat.to_quaternion() 1037 1038 #if bone.parent: 1039 # co.x, co.y, co.z = -co.y, -co.x, co.z 1040 # rot.w, rot.x, rot.y, rot.z = rot.w, rot.y, rot.x, -rot.z 1041 #else: 1042 # co.x, co.y, co.z = -co.x, co.z, -co.y 1043 # 1044 # fix_quat = compat.Z_UP_TO_Y_UP_QUAT #mathutils.Euler((0, 0, math.radians(-90)), 'XYZ').to_quaternion() 1045 # fix_quat2 = compat.BLEND_TO_OPENGL_QUAT #mathutils.Euler((math.radians(-90), 0, 0), 'XYZ').to_quaternion() 1046 # rot = compat.mul3(rot, fix_quat, fix_quat2) 1047 # #rot = compat.mul3(fix_quat2, rot, fix_quat) 1048 # 1049 # rot.w, rot.x, rot.y, rot.z = -rot.y, -rot.z, -rot.x, rot.w 1050 1051 # luvoid : I copied this from the Bone-Util Addon by trzr 1052 #if bone.parent: 1053 # co.x, co.y, co.z = -co.y, co.z, co.x 1054 # rot.w, rot.x, rot.y, rot.z = rot.w, rot.y, -rot.z, -rot.x 1055 #else: 1056 # co.x, co.y, co.z = -co.x, co.z, -co.y 1057 # 1058 # rot = compat.mul(rot, mathutils.Quaternion((0, 0, 1), math.radians(90))) 1059 # rot.w, rot.x, rot.y, rot.z = -rot.w, -rot.x, rot.z, -rot.y 1060 1061 #opengl_mat = compat.convert_blend_z_up_to_opengl_y_up_mat4(bone.matrix) 1062 # 1063 #if bone.parent: 1064 # opengl_mat = compat.mul(compat.convert_blend_z_up_to_opengl_y_up_mat4(bone.parent.matrix).inverted(), opengl_mat) 1065 # 1066 #co = opengl_mat.to_translation() * self.scale 1067 #rot = opengl_mat.to_quaternion() 1068 1069 data = { 1070 'name': common.encode_bone_name(bone.name, self.is_convert_bone_weight_names), 1071 'scl': is_scl_bone, 1072 'parent_index': parent_index, 1073 'co': co.copy(), 1074 'rot': rot.copy(), 1075 } 1076 scale = arm.edit_bones[bone.name].get('cm3d2_bone_scale') 1077 if scale: 1078 data['scale'] = scale 1079 bone_data.append(data) 1080 1081 bpy.ops.object.mode_set(mode=pre_mode) 1082 compat.set_active(context, pre_active) 1083 return bone_data 1084 1085 @staticmethod 1086 def bone_data_parser(container): 1087 """BoneData テキストをパースして辞書を要素とするリストを返す""" 1088 bone_data = [] 1089 bone_name_indices = {} 1090 for line in container: 1091 data = line.split(',') 1092 if len(data) < 5: 1093 continue 1094 1095 parent_name = data[2] 1096 if parent_name.isdigit(): 1097 parent_index = int(parent_name) 1098 else: 1099 parent_index = bone_name_indices.get(parent_name, -1) 1100 1101 bone_datum = { 1102 'name': data[0], 1103 'scl': int(data[1]), 1104 'parent_index': parent_index, 1105 'co': list(map(float, data[3].split())), 1106 'rot': list(map(float, data[4].split())), 1107 } 1108 # scale info (for version 2001 or later) 1109 if len(data) >= 7: 1110 if data[5] == '1': 1111 bone_scale = data[6] 1112 bone_datum['scale'] = list(map(float, bone_scale.split())) 1113 bone_data.append(bone_datum) 1114 bone_name_indices[data[0]] = len(bone_name_indices) 1115 return bone_data 1116 1117 def armature_local_bone_data_parser(self, ob): 1118 """アーマチュアを解析してBoneDataを返す""" 1119 arm = ob.data 1120 1121 # XXX Instead of just adding all bones, only bones / bones-with-decendants 1122 # that have use_deform == True or mathcing vertex groups should be used 1123 bones = [] 1124 bone_name_indices = {} 1125 already_bone_names = [] 1126 bones_queue = arm.bones[:] 1127 while len(bones_queue): 1128 bone = bones_queue.pop(0) 1129 1130 if not bone.parent: 1131 already_bone_names.append(bone.name) 1132 bones.append(bone) 1133 bone_name_indices[bone.name] = len(bone_name_indices) 1134 continue 1135 elif bone.parent.name in already_bone_names: 1136 already_bone_names.append(bone.name) 1137 bones.append(bone) 1138 bone_name_indices[bone.name] = len(bone_name_indices) 1139 continue 1140 1141 bones_queue.append(bone) 1142 1143 local_bone_data = [] 1144 for bone in bones: 1145 mat = bone.matrix_local.copy() 1146 mat = compat.mul(mathutils.Matrix.Scale(-1, 4, (1, 0, 0)), mat) 1147 mat = compat.convert_bl_to_cm_bone_rotation(mat) 1148 pos = mat.translation.copy() 1149 1150 mat.transpose() 1151 mat.row[3] = (0.0, 0.0, 0.0, 1.0) 1152 pos = compat.mul(mat.to_3x3(), pos) 1153 pos *= -self.scale 1154 mat.translation = pos 1155 mat.transpose() 1156 1157 #co = mat.to_translation() * self.scale 1158 #rot = mat.to_quaternion() 1159 # 1160 #co.rotate(rot.inverted()) 1161 #co.x, co.y, co.z = co.y, co.x, -co.z 1162 # 1163 #fix_quat = mathutils.Euler((0, 0, math.radians(-90)), 'XYZ').to_quaternion() 1164 #rot = compat.mul(rot, fix_quat) 1165 #rot.w, rot.x, rot.y, rot.z = -rot.z, -rot.y, -rot.x, rot.w 1166 # 1167 #co_mat = mathutils.Matrix.Translation(co) 1168 #rot_mat = rot.to_matrix().to_4x4() 1169 #mat = compat.mul(co_mat, rot_mat) 1170 # 1171 #copy_mat = mat.copy() 1172 #mat[0][0], mat[0][1], mat[0][2], mat[0][3] = copy_mat[0][0], copy_mat[1][0], copy_mat[2][0], copy_mat[3][0] 1173 #mat[1][0], mat[1][1], mat[1][2], mat[1][3] = copy_mat[0][1], copy_mat[1][1], copy_mat[2][1], copy_mat[3][1] 1174 #mat[2][0], mat[2][1], mat[2][2], mat[2][3] = copy_mat[0][2], copy_mat[1][2], copy_mat[2][2], copy_mat[3][2] 1175 #mat[3][0], mat[3][1], mat[3][2], mat[3][3] = copy_mat[0][3], copy_mat[1][3], copy_mat[2][3], copy_mat[3][3] 1176 1177 mat_array = [] 1178 for vec in mat: 1179 mat_array.extend(vec[:]) 1180 1181 local_bone_data.append({ 1182 'name': common.encode_bone_name(bone.name, self.is_convert_bone_weight_names), 1183 'matrix': mat_array, 1184 }) 1185 return local_bone_data 1186 1187 @staticmethod 1188 def local_bone_data_parser(container): 1189 """LocalBoneData テキストをパースして辞書を要素とするリストを返す""" 1190 local_bone_data = [] 1191 for line in container: 1192 data = line.split(',') 1193 if len(data) != 2: 1194 continue 1195 local_bone_data.append({ 1196 'name': data[0], 1197 'matrix': list(map(float, data[1].split())), 1198 }) 1199 return local_bone_data 1200 1201 @staticmethod 1202 def indexed_data_generator(container, prefix='', max_index=9**9, max_pass=50): 1203 """コンテナ内の数値インデックスをキーに持つ要素を昇順に返すジェネレーター""" 1204 pass_count = 0 1205 for i in range(max_index): 1206 name = prefix + str(i) 1207 if name not in container: 1208 pass_count += 1 1209 if max_pass < pass_count: 1210 return 1211 continue 1212 yield container[name]
filepath: <_PropertyDeferred, <built-in function StringProperty>, {'subtype': 'FILE_PATH', 'attr': 'filepath'}> =
<_PropertyDeferred, <built-in function StringProperty>, {'subtype': 'FILE_PATH', 'attr': 'filepath'}>
filter_glob: <_PropertyDeferred, <built-in function StringProperty>, {'default': '*.model', 'options': {'HIDDEN'}, 'attr': 'filter_glob'}> =
<_PropertyDeferred, <built-in function StringProperty>, {'default': '*.model', 'options': {'HIDDEN'}, 'attr': 'filter_glob'}>
scale: <_PropertyDeferred, <built-in function FloatProperty>, {'name': '倍率', 'default': 0.2, 'min': 0.01, 'max': 100, 'soft_min': 0.01, 'soft_max': 100, 'step': 10, 'precision': 2, 'description': 'エクスポート時のメッシュ等の拡大率です', 'attr': 'scale'}> =
<_PropertyDeferred, <built-in function FloatProperty>, {'name': '倍率', 'default': 0.2, 'min': 0.01, 'max': 100, 'soft_min': 0.01, 'soft_max': 100, 'step': 10, 'precision': 2, 'description': 'エクスポート時のメッシュ等の拡大率です', 'attr': 'scale'}>
is_backup: <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'ファイルをバックアップ', 'default': True, 'description': 'ファイルに上書きする場合にバックアップファイルを複製します', 'attr': 'is_backup'}> =
<_PropertyDeferred, <built-in function BoolProperty>, {'name': 'ファイルをバックアップ', 'default': True, 'description': 'ファイルに上書きする場合にバックアップファイルを複製します', 'attr': 'is_backup'}>
version: <_PropertyDeferred, <built-in function EnumProperty>, {'name': 'ファイルバージョン', 'items': [('2001', '2001', 'model version 2001 (available only for com3d2)', 'NONE', 0), ('2000', '2000', 'model version 2000 (com3d2 version)', 'NONE', 1), ('1000', '1000', 'model version 1000 (available for cm3d2/com3d2)', 'NONE', 2)], 'default': '1000', 'attr': 'version'}> =
<_PropertyDeferred, <built-in function EnumProperty>, {'name': 'ファイルバージョン', 'items': [('2001', '2001', 'model version 2001 (available only for com3d2)', 'NONE', 0), ('2000', '2000', 'model version 2000 (com3d2 version)', 'NONE', 1), ('1000', '1000', 'model version 1000 (available for cm3d2/com3d2)', 'NONE', 2)], 'default': '1000', 'attr': 'version'}>
model_name: <_PropertyDeferred, <built-in function StringProperty>, {'name': 'model名', 'default': '*', 'attr': 'model_name'}> =
<_PropertyDeferred, <built-in function StringProperty>, {'name': 'model名', 'default': '*', 'attr': 'model_name'}>
base_bone_name: <_PropertyDeferred, <built-in function StringProperty>, {'name': '基点ボーン名', 'default': '*', 'attr': 'base_bone_name'}> =
<_PropertyDeferred, <built-in function StringProperty>, {'name': '基点ボーン名', 'default': '*', 'attr': 'base_bone_name'}>
bone_info_mode: <_PropertyDeferred, <built-in function EnumProperty>, {'items': [('ARMATURE', 'アーマチュア', '', 'OUTLINER_OB_ARMATURE', 1), ('TEXT', 'テキスト', '', 'FILE_TEXT', 2), ('OBJECT_PROPERTY', 'オブジェクト内プロパティ', '', 'OBJECT_DATAMODE', 3), ('ARMATURE_PROPERTY', 'アーマチュア内プロパティ', '', 'ARMATURE_DATA', 4)], 'name': 'ボーン情報元', 'default': 'OBJECT_PROPERTY', 'description': 'modelファイルに必要なボーン情報をどこから引っ張ってくるか選びます', 'attr': 'bone_info_mode'}> =
<_PropertyDeferred, <built-in function EnumProperty>, {'items': [('ARMATURE', 'アーマチュア', '', 'OUTLINER_OB_ARMATURE', 1), ('TEXT', 'テキスト', '', 'FILE_TEXT', 2), ('OBJECT_PROPERTY', 'オブジェクト内プロパティ', '', 'OBJECT_DATAMODE', 3), ('ARMATURE_PROPERTY', 'アーマチュア内プロパティ', '', 'ARMATURE_DATA', 4)], 'name': 'ボーン情報元', 'default': 'OBJECT_PROPERTY', 'description': 'modelファイルに必要なボーン情報をどこから引っ張ってくるか選びます', 'attr': 'bone_info_mode'}>
mate_info_mode: <_PropertyDeferred, <built-in function EnumProperty>, {'items': [('TEXT', 'テキスト', '', 'FILE_TEXT', 1), ('MATERIAL', 'マテリアル', '', 'MATERIAL', 2)], 'name': 'マテリアル情報元', 'default': 'MATERIAL', 'description': 'modelファイルに必要なマテリアル情報をどこから引っ張ってくるか選びます', 'attr': 'mate_info_mode'}> =
<_PropertyDeferred, <built-in function EnumProperty>, {'items': [('TEXT', 'テキスト', '', 'FILE_TEXT', 1), ('MATERIAL', 'マテリアル', '', 'MATERIAL', 2)], 'name': 'マテリアル情報元', 'default': 'MATERIAL', 'description': 'modelファイルに必要なマテリアル情報をどこから引っ張ってくるか選びます', 'attr': 'mate_info_mode'}>
is_arrange_name: <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'データ名の連番を削除', 'default': True, 'description': '「○○.001」のような連番が付属したデータ名からこれらを削除します', 'attr': 'is_arrange_name'}> =
<_PropertyDeferred, <built-in function BoolProperty>, {'name': 'データ名の連番を削除', 'default': True, 'description': '「○○.001」のような連番が付属したデータ名からこれらを削除します', 'attr': 'is_arrange_name'}>
is_align_to_base_bone: <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'Align to Base Bone', 'default': True, 'description': "Align the object to it's base bone", 'attr': 'is_align_to_base_bone'}> =
<_PropertyDeferred, <built-in function BoolProperty>, {'name': 'Align to Base Bone', 'default': True, 'description': "Align the object to it's base bone", 'attr': 'is_align_to_base_bone'}>
is_convert_tris: <_PropertyDeferred, <built-in function BoolProperty>, {'name': '四角面を三角面に', 'default': True, 'description': '四角ポリゴンを三角ポリゴンに変換してから出力します、元のメッシュには影響ありません', 'attr': 'is_convert_tris'}> =
<_PropertyDeferred, <built-in function BoolProperty>, {'name': '四角面を三角面に', 'default': True, 'description': '四角ポリゴンを三角ポリゴンに変換してから出力します、元のメッシュには影響ありません', 'attr': 'is_convert_tris'}>
is_split_sharp: <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'Split Sharp Edges', 'default': True, 'description': 'Split all edges marked as sharp.', 'attr': 'is_split_sharp'}> =
<_PropertyDeferred, <built-in function BoolProperty>, {'name': 'Split Sharp Edges', 'default': True, 'description': 'Split all edges marked as sharp.', 'attr': 'is_split_sharp'}>
is_normalize_weight: <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'ウェイトの合計を1.0に', 'default': True, 'description': '4つのウェイトの合計値が1.0になるように正規化します', 'attr': 'is_normalize_weight'}> =
<_PropertyDeferred, <built-in function BoolProperty>, {'name': 'ウェイトの合計を1.0に', 'default': True, 'description': '4つのウェイトの合計値が1.0になるように正規化します', 'attr': 'is_normalize_weight'}>
is_convert_bone_weight_names: <_PropertyDeferred, <built-in function BoolProperty>, {'name': '頂点グループ名をCM3D2用に変換', 'default': True, 'description': '全ての頂点グループ名をCM3D2で使える名前にしてからエクスポートします', 'attr': 'is_convert_bone_weight_names'}> =
<_PropertyDeferred, <built-in function BoolProperty>, {'name': '頂点グループ名をCM3D2用に変換', 'default': True, 'description': '全ての頂点グループ名をCM3D2で使える名前にしてからエクスポートします', 'attr': 'is_convert_bone_weight_names'}>
is_clean_vertex_groups: <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'クリーンな頂点グループ', 'default': True, 'description': '重みがゼロの場合、頂点グループから頂点を削除します', 'attr': 'is_clean_vertex_groups'}> =
<_PropertyDeferred, <built-in function BoolProperty>, {'name': 'クリーンな頂点グループ', 'default': True, 'description': '重みがゼロの場合、頂点グループから頂点を削除します', 'attr': 'is_clean_vertex_groups'}>
is_batch: <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'バッチモード', 'default': False, 'description': 'モードの切替やエラー個所の選択を行いません', 'attr': 'is_batch'}> =
<_PropertyDeferred, <built-in function BoolProperty>, {'name': 'バッチモード', 'default': False, 'description': 'モードの切替やエラー個所の選択を行いません', 'attr': 'is_batch'}>
export_tangent: <_PropertyDeferred, <built-in function BoolProperty>, {'name': '接空間情報出力', 'default': False, 'description': '接空間情報(binormals, tangents)を出力する', 'attr': 'export_tangent'}> =
<_PropertyDeferred, <built-in function BoolProperty>, {'name': '接空間情報出力', 'default': False, 'description': '接空間情報(binormals, tangents)を出力する', 'attr': 'export_tangent'}>
shapekey_threshold: <_PropertyDeferred, <built-in function FloatProperty>, {'name': 'Shape Key Threshold', 'default': 0.001, 'min': 0, 'soft_min': 0.0005, 'max': 0.01, 'soft_max': 0.002, 'precision': 5, 'description': 'Lower values increase accuracy and file size. Higher values truncate small changes and reduce file size.', 'attr': 'shapekey_threshold'}> =
<_PropertyDeferred, <built-in function FloatProperty>, {'name': 'Shape Key Threshold', 'default': 0.001, 'min': 0, 'soft_min': 0.0005, 'max': 0.01, 'soft_max': 0.002, 'precision': 5, 'description': 'Lower values increase accuracy and file size. Higher values truncate small changes and reduce file size.', 'attr': 'shapekey_threshold'}>
export_shapekey_normals: <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'Export Shape Key Normals', 'default': True, 'description': 'Export custom normals for each shape key on export.', 'attr': 'export_shapekey_normals'}> =
<_PropertyDeferred, <built-in function BoolProperty>, {'name': 'Export Shape Key Normals', 'default': True, 'description': 'Export custom normals for each shape key on export.', 'attr': 'export_shapekey_normals'}>
shapekey_normals_blend: <_PropertyDeferred, <built-in function FloatProperty>, {'name': 'Shape Key Normals Blend', 'default': 0.6, 'min': 0, 'max': 1, 'precision': 3, 'description': 'Adjust the influence of shape keys on custom normals', 'attr': 'shapekey_normals_blend'}> =
<_PropertyDeferred, <built-in function FloatProperty>, {'name': 'Shape Key Normals Blend', 'default': 0.6, 'min': 0, 'max': 1, 'precision': 3, 'description': 'Adjust the influence of shape keys on custom normals', 'attr': 'shapekey_normals_blend'}>
use_shapekey_colors: <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'Use Shape Key Colors', 'default': True, 'description': 'Use the shape key normals stored in the vertex colors instead of calculating the normals on export. (Recommend disabling if geometry was customized)', 'attr': 'use_shapekey_colors'}> =
<_PropertyDeferred, <built-in function BoolProperty>, {'name': 'Use Shape Key Colors', 'default': True, 'description': 'Use the shape key normals stored in the vertex colors instead of calculating the normals on export. (Recommend disabling if geometry was customized)', 'attr': 'use_shapekey_colors'}>
def
report_cancel(self, report_message, report_type={'ERROR'}, resobj={'CANCELLED'}):
84 def report_cancel(self, report_message, report_type={'ERROR'}, resobj={'CANCELLED'}): 85 """エラーメッセージを出力してキャンセルオブジェクトを返す""" 86 self.report(type=report_type, message=report_message) 87 return resobj
エラーメッセージを出力してキャンセルオブジェクトを返す
def
precheck(self, context):
89 def precheck(self, context): 90 """データの成否チェック""" 91 ob = context.active_object 92 if not ob: 93 return self.report_cancel("アクティブオブジェクトがありません") 94 if ob.type != 'MESH': 95 return self.report_cancel("メッシュオブジェクトを選択した状態で実行してください") 96 if not len(ob.material_slots): 97 return self.report_cancel("マテリアルがありません") 98 for slot in ob.material_slots: 99 if not slot.material: 100 return self.report_cancel("空のマテリアルスロットを削除してください") 101 try: 102 slot.material['shader1'] 103 slot.material['shader2'] 104 except: 105 return self.report_cancel("マテリアルに「shader1」と「shader2」という名前のカスタムプロパティを用意してください") 106 me = ob.data 107 if not me.uv_layers.active: 108 return self.report_cancel("UVがありません") 109 if 65535 < len(me.vertices): 110 return self.report_cancel("エクスポート可能な頂点数を大幅に超えています、最低でも65535未満には削減してください") 111 return None
データの成否チェック
def
invoke(self, context, event):
113 def invoke(self, context, event): 114 res = self.precheck(context) 115 if res: 116 return res 117 ob = context.active_object 118 119 # model名とか 120 ob_names = common.remove_serial_number(ob.name, self.is_arrange_name).split('.') 121 self.model_name = ob_names[0] 122 self.base_bone_name = ob_names[1] if 2 <= len(ob_names) else 'Auto' 123 124 # ボーン情報元のデフォルトオプションを取得 125 arm_ob = ob.parent 126 for mod in ob.modifiers: 127 if mod.type == 'ARMATURE' and mod.object: 128 arm_ob = mod.object 129 if arm_ob and not arm_ob.type == 'ARMATURE': 130 arm_ob = None 131 132 info_mode_was_armature = (self.bone_info_mode == 'ARMATURE') 133 if "BoneData" in context.blend_data.texts: 134 if "LocalBoneData" in context.blend_data.texts: 135 self.bone_info_mode = 'TEXT' 136 if "BoneData:0" in ob: 137 ver = ob.get("ModelVersion") 138 if ver and ver >= 1000: 139 self.version = str(ver) 140 if "LocalBoneData:0" in ob: 141 self.bone_info_mode = 'OBJECT_PROPERTY' 142 if arm_ob: 143 if info_mode_was_armature: 144 self.bone_info_mode = 'ARMATURE' 145 else: 146 self.bone_info_mode = 'ARMATURE_PROPERTY' 147 148 # エクスポート時のデフォルトパスを取得 149 #if not self.filepath[-6:] == '.model': 150 if common.preferences().model_default_path: 151 self.filepath = common.default_cm3d2_dir(common.preferences().model_default_path, self.model_name, "model") 152 else: 153 self.filepath = common.default_cm3d2_dir(common.preferences().model_export_path, self.model_name, "model") 154 155 # バックアップ関係 156 self.is_backup = bool(common.preferences().backup_ext) 157 158 self.scale = 1.0 / common.preferences().scale 159 context.window_manager.fileselect_add(self) 160 return {'RUNNING_MODAL'}
def
draw(self, context):
163 def draw(self, context): 164 self.layout.prop(self, 'scale') 165 row = self.layout.row() 166 row.prop(self, 'is_backup', icon='FILE_BACKUP') 167 if not common.preferences().backup_ext: 168 row.enabled = False 169 self.layout.prop(self, 'is_arrange_name', icon='FILE_TICK') 170 box = self.layout.box() 171 box.prop(self, 'version', icon='LINENUMBERS_ON') 172 box.prop(self, 'model_name', icon='SORTALPHA') 173 174 row = box.row() 175 row.prop(self, 'base_bone_name', icon='CONSTRAINT_BONE') 176 if self.base_bone_name == 'Auto': 177 row.enabled = False 178 179 prefs = common.preferences() 180 181 box = self.layout.box() 182 col = box.column(align=True) 183 col.label(text="ボーン情報元", icon='BONE_DATA') 184 col.prop(self, 'bone_info_mode', icon='BONE_DATA', expand=True) 185 col = box.column(align=True) 186 col.label(text="マテリアル情報元", icon='MATERIAL') 187 col.prop(self, 'mate_info_mode', icon='MATERIAL', expand=True) 188 189 box = self.layout.box() 190 box.label(text="メッシュオプション") 191 box.prop(self , 'is_align_to_base_bone', icon=compat.icon('OBJECT_ORIGIN' )) 192 box.prop(self , 'is_convert_tris' , icon=compat.icon('MESH_DATA' )) 193 box.prop(self , 'is_split_sharp' , icon=compat.icon('MOD_EDGESPLIT' )) 194 box.prop(self , 'export_tangent' , icon=compat.icon('CURVE_BEZCIRCLE')) 195 sub_box = box.box() 196 sub_box.prop(self , 'shapekey_threshold' , icon=compat.icon('SHAPEKEY_DATA' ), slider=True) 197 sub_box.prop(prefs, 'skip_shapekey' , icon=compat.icon('SHAPEKEY_DATA' ), toggle=1) 198 sub_box.prop(self , 'export_shapekey_normals', icon=compat.icon('NORMALS_VERTEX_FACE')) 199 row = sub_box.row() 200 row .prop(self , 'shapekey_normals_blend' , icon=compat.icon('MOD_NORMALEDIT' ), slider=True) 201 row.enabled = self.export_shapekey_normals 202 row = sub_box.row() 203 row .prop(self , 'use_shapekey_colors' , icon=compat.icon('GROUP_VCOL') , toggle=0) 204 row.enabled = self.export_shapekey_normals 205 sub_box = box.box() 206 sub_box.prop(self, 'is_normalize_weight', icon='MOD_VERTEX_WEIGHT') 207 sub_box.prop(self, 'is_clean_vertex_groups', icon='MOD_VERTEX_WEIGHT') 208 sub_box.prop(self, 'is_convert_bone_weight_names', icon_value=common.kiss_icon()) 209 sub_box 210 sub_box = box.box() 211 sub_box.prop(prefs, 'is_apply_modifiers', icon='MODIFIER') 212 row = sub_box.row() 213 row.prop(prefs, 'custom_normal_blend', icon='SNAP_NORMAL', slider=True) 214 row.enabled = prefs.is_apply_modifiers
def
execute(self, context):
225 def execute(self, context): 226 start_time = time.time() 227 prefs = common.preferences() 228 229 selected_objs = context.selected_objects 230 source_objs = [] 231 prev_mode = None 232 try: 233 ob_source = context.active_object 234 if ob_source not in selected_objs: 235 selected_objs.append(ob_source) # luvoid : Fix error where object is active but not selected 236 ob_name = ob_source.name 237 ob_main = None 238 if self.is_batch: 239 # アクティブオブジェクトを1つコピーするだけでjoinしない 240 source_objs.append(ob_source) 241 compat.set_select(ob_source, False) 242 ob_main = self.copy_and_activate_ob(context, ob_source) 243 244 if prefs.is_apply_modifiers and bpy.ops.object.forced_modifier_apply.poll(context): 245 bpy.ops.object.forced_modifier_apply(is_applies=[True for i in range(32)]) 246 else: 247 selected_count = 0 248 # 選択されたMESHオブジェクトをコピーしてjoin 249 # 必要に応じて、モディファイアの強制適用を行う 250 for selected in selected_objs: 251 source_objs.append(selected) 252 253 compat.set_select(selected, False) 254 255 if selected.type == 'MESH': 256 ob_created = self.copy_and_activate_ob(context, selected) 257 if selected == ob_source: 258 ob_main = ob_created 259 if prefs.is_apply_modifiers: 260 bpy.ops.object.forced_modifier_apply(apply_viewport_visible=True) 261 262 selected_count += 1 263 264 mode = context.active_object.mode 265 if mode != 'OBJECT': 266 prev_mode = mode 267 bpy.ops.object.mode_set(mode='OBJECT') 268 269 if selected_count > 1: 270 if ob_main: 271 compat.set_active(context, ob_main) 272 bpy.ops.object.join() 273 self.report(type={'INFO'}, message=f_tip_("{}個のオブジェクトをマージしました", selected_count)) 274 275 ret = self.export(context, ob_main) 276 if 'FINISHED' not in ret: 277 return ret 278 279 context.window_manager.progress_update(10) 280 diff_time = time.time() - start_time 281 self.report(type={'INFO'}, message=f_tip_("modelのエクスポートが完了しました。{:.2f} 秒 file={}", diff_time, self.filepath)) 282 return ret 283 finally: 284 # 作業データの破棄(コピーデータを削除、選択状態の復元、アクティブオブジェクト、モードの復元) 285 if ob_main: 286 common.remove_data(ob_main) 287 # me_copied = ob_main.data 288 # context.blend_data.objects.remove(ob_main, do_unlink=True) 289 # context.blend_data.meshes.remove(me_copied, do_unlink=True) 290 291 for obj in source_objs: 292 compat.set_select(obj, True) 293 294 if ob_source: 295 # TODO 元のオブジェクトをアクティブに戻す 296 if ob_name in bpy.data.objects: 297 compat.set_active(context, ob_source) 298 299 if prev_mode: 300 bpy.ops.object.mode_set(mode=prev_mode)
def
export(self, context, ob):
302 def export(self, context, ob): 303 """モデルファイルを出力""" 304 prefs = common.preferences() 305 306 if not self.is_batch: 307 prefs.model_export_path = self.filepath 308 prefs.scale = 1.0 / self.scale 309 310 context.window_manager.progress_begin(0, 10) 311 context.window_manager.progress_update(0) 312 313 res = self.precheck(context) 314 if res: 315 return res 316 me = ob.data 317 318 if ob.active_shape_key_index != 0: 319 ob.active_shape_key_index = 0 320 me.update() 321 322 # データの成否チェック 323 if self.bone_info_mode == 'ARMATURE': 324 arm_ob = ob.parent 325 if arm_ob and arm_ob.type != 'ARMATURE': 326 return self.report_cancel("メッシュオブジェクトの親がアーマチュアではありません") 327 if not arm_ob: 328 try: 329 arm_ob = next(mod for mod in ob.modifiers if mod.type == 'ARMATURE' and mod.object) 330 except StopIteration: 331 return self.report_cancel("アーマチュアが見つかりません、親にするかモディファイアにして下さい") 332 arm_ob = arm_ob.object 333 elif self.bone_info_mode == 'TEXT': 334 if "BoneData" not in context.blend_data.texts: 335 return self.report_cancel("テキスト「BoneData」が見つかりません、中止します") 336 if "LocalBoneData" not in context.blend_data.texts: 337 return self.report_cancel("テキスト「LocalBoneData」が見つかりません、中止します") 338 elif self.bone_info_mode == 'OBJECT_PROPERTY': 339 if "BoneData:0" not in ob: 340 return self.report_cancel("オブジェクトのカスタムプロパティにボーン情報がありません") 341 if "LocalBoneData:0" not in ob: 342 return self.report_cancel("オブジェクトのカスタムプロパティにボーン情報がありません") 343 elif self.bone_info_mode == 'ARMATURE_PROPERTY': 344 arm_ob = ob.parent 345 if arm_ob and arm_ob.type != 'ARMATURE': 346 return self.report_cancel("メッシュオブジェクトの親がアーマチュアではありません") 347 if not arm_ob: 348 try: 349 arm_ob = next(mod for mod in ob.modifiers if mod.type == 'ARMATURE' and mod.object) 350 except StopIteration: 351 return self.report_cancel("アーマチュアが見つかりません、親にするかモディファイアにして下さい") 352 arm_ob = arm_ob.object 353 if "BoneData:0" not in arm_ob.data: 354 return self.report_cancel("アーマチュアのカスタムプロパティにボーン情報がありません") 355 if "LocalBoneData:0" not in arm_ob.data: 356 return self.report_cancel("アーマチュアのカスタムプロパティにボーン情報がありません") 357 else: 358 return self.report_cancel("ボーン情報元のモードがおかしいです") 359 360 if self.mate_info_mode == 'TEXT': 361 for index, slot in enumerate(ob.material_slots): 362 if "Material:" + str(index) not in context.blend_data.texts: 363 return self.report_cancel("マテリアル情報元のテキストが足りません") 364 context.window_manager.progress_update(1) 365 366 # model名とか 367 ob_names = common.remove_serial_number(ob.name, self.is_arrange_name).split('.') 368 if self.model_name == '*': 369 self.model_name = ob_names[0] 370 if self.base_bone_name == '*': 371 self.base_bone_name = ob_names[1] if 2 <= len(ob_names) else 'Auto' 372 373 # BoneData情報読み込み 374 base_bone_candidate = None 375 bone_data = [] 376 if self.bone_info_mode == 'ARMATURE': 377 bone_data = self.armature_bone_data_parser(context, arm_ob) 378 base_bone_candidate = arm_ob.data['BaseBone'] 379 elif self.bone_info_mode == 'TEXT': 380 bone_data_text = context.blend_data.texts["BoneData"] 381 if 'BaseBone' in bone_data_text: 382 base_bone_candidate = bone_data_text['BaseBone'] 383 bone_data = self.bone_data_parser(l.body for l in bone_data_text.lines) 384 elif self.bone_info_mode in ['OBJECT_PROPERTY', 'ARMATURE_PROPERTY']: 385 target = ob if self.bone_info_mode == 'OBJECT_PROPERTY' else arm_ob.data 386 if 'BaseBone' in target: 387 base_bone_candidate = target['BaseBone'] 388 bone_data = self.bone_data_parser(self.indexed_data_generator(target, prefix="BoneData:")) 389 if len(bone_data) <= 0: 390 return self.report_cancel("テキスト「BoneData」に有効なデータがありません") 391 392 if self.base_bone_name not in (b['name'] for b in bone_data): 393 if base_bone_candidate and self.base_bone_name == 'Auto': 394 self.base_bone_name = base_bone_candidate 395 else: 396 return self.report_cancel("基点ボーンが存在しません") 397 bone_name_indices = {bone['name']: index for index, bone in enumerate(bone_data)} 398 context.window_manager.progress_update(2) 399 400 if self.is_align_to_base_bone: 401 bpy.ops.object.align_to_cm3d2_base_bone(scale=1.0/self.scale, is_preserve_mesh=True, bone_info_mode=self.bone_info_mode) 402 me.update() 403 404 if self.is_split_sharp: 405 bpy.ops.object.mode_set(mode='EDIT') 406 bpy.ops.mesh.split_sharp() 407 bpy.ops.object.mode_set(mode='OBJECT') 408 409 # LocalBoneData情報読み込み 410 local_bone_data = [] 411 if self.bone_info_mode == 'ARMATURE': 412 local_bone_data = self.armature_local_bone_data_parser(arm_ob) 413 elif self.bone_info_mode == 'TEXT': 414 local_bone_data_text = context.blend_data.texts["LocalBoneData"] 415 local_bone_data = self.local_bone_data_parser(l.body for l in local_bone_data_text.lines) 416 elif self.bone_info_mode in ['OBJECT_PROPERTY', 'ARMATURE_PROPERTY']: 417 target = ob if self.bone_info_mode == 'OBJECT_PROPERTY' else arm_ob.data 418 local_bone_data = self.local_bone_data_parser(self.indexed_data_generator(target, prefix="LocalBoneData:")) 419 if len(local_bone_data) <= 0: 420 return self.report_cancel("テキスト「LocalBoneData」に有効なデータがありません") 421 local_bone_name_indices = {bone['name']: index for index, bone in enumerate(local_bone_data)} 422 context.window_manager.progress_update(3) 423 424 used_local_bone = {index: False for index, bone in enumerate(local_bone_data)} 425 426 # ウェイト情報読み込み 427 vertices = [] 428 is_over_one = 0 429 is_under_one = 0 430 is_in_too_many = 0 431 for i, vert in enumerate(me.vertices): 432 vgs = [] 433 for vg in vert.groups: 434 if len(ob.vertex_groups) <= vg.group: # Apparently a vertex can be assigned to a non-existent group. 435 continue 436 name = common.encode_bone_name(ob.vertex_groups[vg.group].name, self.is_convert_bone_weight_names) 437 index = local_bone_name_indices.get(name, -1) 438 if index >= 0 and (vg.weight > 0.0 or not self.is_clean_vertex_groups): 439 vgs.append([index, vg.weight]) 440 # luvoid : track used bones 441 used_local_bone[index] = True 442 boneindex = bone_name_indices.get(name, -1) 443 while boneindex >= 0: 444 parent = bone_data[boneindex] 445 localindex = local_bone_name_indices.get(parent['name'], -1) 446 # could check for `localindex == -1` here, but it's prescence may be useful in determing if the local bones resolve back to some root 447 used_local_bone[localindex] = True 448 boneindex = parent['parent_index'] 449 if len(vgs) == 0: 450 if not self.is_batch: 451 self.select_no_weight_vertices(context, local_bone_name_indices) 452 return self.report_cancel("ウェイトが割り当てられていない頂点が見つかりました、中止します") 453 if len(vgs) > 4: 454 is_in_too_many += 1 455 vgs = sorted(vgs, key=itemgetter(1), reverse=True)[0:4] 456 total = sum(vg[1] for vg in vgs) 457 if self.is_normalize_weight: 458 for vg in vgs: 459 vg[1] /= total 460 else: 461 if 1.01 < total: 462 is_over_one += 1 463 elif total < 0.99: 464 is_under_one += 1 465 if len(vgs) < 4: 466 vgs += [(0, 0.0)] * (4 - len(vgs)) 467 vertices.append({ 468 'index': vert.index, 469 'face_indexs': list(map(itemgetter(0), vgs)), 470 'weights': list(map(itemgetter(1), vgs)), 471 }) 472 473 if 1 <= is_over_one: 474 self.report(type={'WARNING'}, message=f_tip_("ウェイトの合計が1.0を超えている頂点が見つかりました。正規化してください。超過している頂点の数:{}", is_over_one)) 475 if 1 <= is_under_one: 476 self.report(type={'WARNING'}, message=f_tip_("ウェイトの合計が1.0未満の頂点が見つかりました。正規化してください。不足している頂点の数:{}", is_under_one)) 477 478 # luvoid : warn that there are vertices in too many vertex groups 479 if is_in_too_many > 0: 480 self.report(type={'WARNING'}, message=f_tip_("4つを超える頂点グループにある頂点が見つかりました。頂点グループをクリーンアップしてください。不足している頂点の数:{}", is_in_too_many)) 481 482 # luvoid : check for unused local bones that the game will delete 483 is_deleted = 0 484 deleted_names = "The game will delete these local bones" 485 for index, is_used in used_local_bone.items(): 486 print(index, is_used) 487 if is_used == False: 488 is_deleted += 1 489 deleted_names = deleted_names + '\n' + local_bone_data[index]['name'] 490 elif is_used == True: 491 pass 492 else: 493 print(f_tip_("Unexpected: used_local_bone[{key}] == {value} when len(used_local_bone) == {length}", key=index, value=is_used, length=len(used_local_bone))) 494 self.report(type={'WARNING'}, message=f_tip_("Could not find whether bone with index {index} was used. See console for more info.", index=i)) 495 if is_deleted > 0: 496 self.report(type={'WARNING'}, message=f_tip_("頂点が割り当てられていない{num}つのローカルボーンが見つかりました。 詳細については、ログを参照してください。", num=is_deleted)) 497 self.report(type={'INFO'}, message=deleted_names) 498 499 context.window_manager.progress_update(4) 500 501 502 try: 503 writer = common.open_temporary(self.filepath, 'wb', is_backup=self.is_backup) 504 except: 505 self.report(type={'ERROR'}, message=f_tip_("ファイルを開くのに失敗しました、アクセス不可かファイルが存在しません。file={}", self.filepath)) 506 return {'CANCELLED'} 507 508 model_datas = { 509 'bone_data': bone_data, 510 'local_bone_data': local_bone_data, 511 'vertices': vertices, 512 } 513 try: 514 with writer: 515 self.write_model(context, ob, writer, **model_datas) 516 except common.CM3D2ExportException as e: 517 self.report(type={'ERROR'}, message=str(e)) 518 return {'CANCELLED'} 519 520 return {'FINISHED'}
モデルファイルを出力
def
write_model( self, context, ob, writer, bone_data=[], local_bone_data=[], vertices=[]):
522 def write_model(self, context, ob, writer, bone_data=[], local_bone_data=[], vertices=[]): 523 """モデルデータをファイルオブジェクトに書き込む""" 524 me = ob.data 525 prefs = common.preferences() 526 527 # ファイル先頭 528 common.write_str(writer, 'CM3D2_MESH') 529 self.version_num = int(self.version) 530 writer.write(struct.pack('<i', self.version_num)) 531 532 common.write_str(writer, self.model_name) 533 common.write_str(writer, self.base_bone_name) 534 535 # ボーン情報書き出し 536 writer.write(struct.pack('<i', len(bone_data))) 537 for bone in bone_data: 538 common.write_str(writer, bone['name']) 539 writer.write(struct.pack('<b', bone['scl'])) 540 context.window_manager.progress_update(3.3) 541 for bone in bone_data: 542 writer.write(struct.pack('<i', bone['parent_index'])) 543 context.window_manager.progress_update(3.7) 544 for bone in bone_data: 545 writer.write(struct.pack('<3f', bone['co'][0], bone['co'][1], bone['co'][2])) 546 writer.write(struct.pack('<4f', bone['rot'][1], bone['rot'][2], bone['rot'][3], bone['rot'][0])) 547 if self.version_num >= 2001: 548 use_scale = ('scale' in bone) 549 writer.write(struct.pack('<b', use_scale)) 550 if use_scale: 551 bone_scale = bone['scale'] 552 writer.write(struct.pack('<3f', bone_scale[0], bone_scale[1], bone_scale[2])) 553 context.window_manager.progress_update(4) 554 555 # 正しい頂点数などを取得 556 bm = bmesh.new() 557 bm.from_mesh(me) 558 uv_lay = bm.loops.layers.uv.active 559 vert_uvs = [] 560 vert_uvs_append = vert_uvs.append 561 vert_iuv = {} 562 vert_indices = {} 563 vert_count = 0 564 for vert in bm.verts: 565 vert_uv = [] 566 vert_uvs_append(vert_uv) 567 for loop in vert.link_loops: 568 uv = loop[uv_lay].uv 569 if uv not in vert_uv: 570 vert_uv.append(uv) 571 vert_iuv[hash((vert.index, uv.x, uv.y))] = vert_count 572 vert_indices[vert.index] = vert_count 573 vert_count += 1 574 if 65535 < vert_count: 575 raise common.CM3D2ExportException(f_tip_("頂点数がまだ多いです (現在{}頂点)。あと{}頂点以上減らしてください、中止します", vert_count, vert_count - 65535)) 576 context.window_manager.progress_update(5) 577 578 writer.write(struct.pack('<2i', vert_count, len(ob.material_slots))) 579 580 # ローカルボーン情報を書き出し 581 writer.write(struct.pack('<i', len(local_bone_data))) 582 for bone in local_bone_data: 583 common.write_str(writer, bone['name']) 584 context.window_manager.progress_update(5.3) 585 for bone in local_bone_data: 586 for f in bone['matrix']: 587 writer.write(struct.pack('<f', f)) 588 context.window_manager.progress_update(5.7) 589 590 # カスタム法線情報を取得 591 if me.has_custom_normals: 592 custom_normals = [mathutils.Vector() for i in range(len(me.vertices))] 593 me.calc_normals_split() 594 for loop in me.loops: 595 custom_normals[loop.vertex_index] += loop.normal 596 for no in custom_normals: 597 no.normalize() 598 else: 599 custom_normals = None 600 601 cm_verts = [] 602 cm_norms = [] 603 cm_uvs = [] 604 # 頂点情報を書き出し 605 for i, vert in enumerate(bm.verts): 606 co = compat.convert_bl_to_cm_space( vert.co * self.scale ) 607 if me.has_custom_normals: 608 no = custom_normals[vert.index] 609 else: 610 no = vert.normal.copy() 611 no = compat.convert_bl_to_cm_space( no ) 612 for uv in vert_uvs[i]: 613 cm_verts.append(co) 614 cm_norms.append(no) 615 cm_uvs.append(uv) 616 writer.write(struct.pack('<3f', co.x, co.y, co.z)) 617 writer.write(struct.pack('<3f', no.x, no.y, no.z)) 618 writer.write(struct.pack('<2f', uv.x, uv.y)) 619 context.window_manager.progress_update(6) 620 621 cm_tris = self.parse_triangles(bm, ob, uv_lay, vert_iuv, vert_indices) 622 623 # 接空間情報を書き出し 624 if self.export_tangent: 625 tangents = self.calc_tangents(cm_tris, cm_verts, cm_norms, cm_uvs) 626 writer.write(struct.pack('<i', len(tangents))) 627 for t in tangents: 628 writer.write(struct.pack('<4f', *t)) 629 else: 630 writer.write(struct.pack('<i', 0)) 631 632 # ウェイト情報を書き出し 633 for vert in vertices: 634 for uv in vert_uvs[vert['index']]: 635 writer.write(struct.pack('<4H', *vert['face_indexs'])) 636 writer.write(struct.pack('<4f', *vert['weights'])) 637 context.window_manager.progress_update(7) 638 639 # 面情報を書き出し 640 for tri in cm_tris: 641 writer.write(struct.pack('<i', len(tri))) 642 for vert_index in tri: 643 writer.write(struct.pack('<H', vert_index)) 644 context.window_manager.progress_update(8) 645 646 # マテリアルを書き出し 647 writer.write(struct.pack('<i', len(ob.material_slots))) 648 for slot_index, slot in enumerate(ob.material_slots): 649 if self.mate_info_mode == 'MATERIAL': 650 mat_data = cm3d2_data.MaterialHandler.parse_mate(slot.material, self.is_arrange_name) 651 mat_data.write(writer, write_header=False) 652 653 elif self.mate_info_mode == 'TEXT': 654 text = context.blend_data.texts["Material:" + str(slot_index)].as_string() 655 mat_data = cm3d2_data.MaterialHandler.parse_text(slot.material, self.is_arrange_name) 656 mat_data.write(writer, write_header=False) 657 658 context.window_manager.progress_update(9) 659 660 # モーフを書き出し 661 if me.shape_keys and len(me.shape_keys.key_blocks) >= 2: 662 try: 663 self.write_shapekeys(context, ob, writer, vert_uvs, custom_normals) 664 finally: 665 print("FINISHED SHAPE KEYS WRITE") 666 pass 667 common.write_str(writer, 'end')
モデルデータをファイルオブジェクトに書き込む
def
write_shapekeys(self, context, ob, writer, vert_uvs, custom_normals=None):
669 def write_shapekeys(self, context, ob, writer, vert_uvs, custom_normals=None): 670 # モーフを書き出し 671 me = ob.data 672 prefs = common.preferences() 673 674 is_use_attributes = (not compat.IS_LEGACY and bpy.app.version >= (2,92)) 675 676 loops_vert_index = np.empty((len(me.loops)), dtype=int) 677 me.loops.foreach_get('vertex_index', loops_vert_index.ravel()) 678 679 def find_normals_attribute(name) -> (bpy.types.Attribute, bool): 680 if is_use_attributes: 681 normals_color = me.attributes[name] if name in me.attributes.keys() else None 682 attribute_is_color = (not normals_color is None) and normals_color.data_type in {'BYTE_COLOR', 'FLOAT_COLOR'} 683 else: 684 normals_color = me.vertex_colors[name] if name in me.vertex_colors.keys() else None 685 attribute_is_color = True 686 return normals_color, attribute_is_color 687 688 if self.use_shapekey_colors: 689 static_attribute_colors = np.empty((len(me.loops), 4), dtype=float) 690 color_offset = np.array([[0.5,0.5,0.5]]) 691 loops_per_vertex = np.zeros((len(me.vertices))) 692 for loop in me.loops: 693 loops_per_vertex[loop.vertex_index] += 1 694 loops_per_vertex_reciprocal = np.reciprocal(loops_per_vertex, out=loops_per_vertex).reshape((len(me.vertices), 1)) 695 def get_sk_delta_normals_from_attribute(attribute, is_color, out): 696 if is_color: 697 attribute.data.foreach_get('color', static_attribute_colors.ravel()) 698 loop_delta_normals = static_attribute_colors[:,:3] 699 loop_delta_normals -= color_offset 700 loop_delta_normals *= 2 701 else: 702 loop_delta_normals = static_attribute_colors[:,:3] 703 attribute.data.foreach_get('vector', loop_delta_normals.ravel()) 704 705 vert_delta_normals = out 706 vert_delta_normals.fill(0) 707 708 # for loop in me.loops: vert_delta_normals[loop.vertex_index] += loop_delta_normals[loop.index] 709 np.add.at(vert_delta_normals, loops_vert_index, loop_delta_normals) # XXX Slower but handles edge cases better 710 #vert_delta_normals[loops_vert_index] += loop_delta_normals # XXX Only first loop's value will be kept 711 712 # for delta_normal in vert_delta_normals: delta_normal /= loops_per_vertex[vert.index] 713 vert_delta_normals *= loops_per_vertex_reciprocal 714 715 return out #.tolist() 716 717 if me.has_custom_normals: 718 basis_custom_normals = np.array(custom_normals, dtype=float) 719 static_loop_normals = np.empty((len(me.loops), 3), dtype=float) 720 static_vert_lengths = np.empty((len(me.vertices), 1), dtype=float) 721 def get_sk_delta_normals_from_custom_normals(shape_key, out): 722 vert_custom_normals = out 723 vert_custom_normals.fill(0) 724 725 loop_custom_normals = static_loop_normals 726 np.copyto(loop_custom_normals.ravel(), shape_key.normals_split_get()) 727 728 # for loop in me.loops: vert_delta_normals[loop.vertex_index] += loop_delta_normals[loop.index] 729 if not self.is_split_sharp: 730 # XXX Slower 731 np.add.at(vert_custom_normals, loops_vert_index, loop_custom_normals) 732 vert_len_sq = get_lengths_squared(vert_custom_normals, out=static_vert_lengths) 733 vert_len = np.sqrt(vert_len_sq, out=vert_len_sq) 734 np.reciprocal(vert_len, out=vert_len) 735 vert_custom_normals *= vert_len #.reshape((*vert_len.shape, 1)) 736 else: 737 # loop normals should be the same per-vertex unless there is a sharp edge 738 # or a flat shaded face, but all sharp edges were split, so this method is fine 739 # (and Flat shaded faces just won't be supported) 740 vert_custom_normals[loops_vert_index] += loop_custom_normals # Only first loop's value will be kept 741 742 vert_custom_normals -= basis_custom_normals 743 return out 744 745 if not me.has_custom_normals: 746 basis_normals = np.empty((len(me.vertices), 3), dtype=float) 747 me.vertices.foreach_get('normal', basis_normals.ravel()) 748 def get_sk_delta_normals_from_normals(shape_key, out): 749 vert_normals = out 750 np.copyto(vert_normals.ravel(), shape_key.normals_vertex_get()) 751 vert_delta_normals = np.subtract(vert_normals, basis_normals, out=out) 752 return out 753 754 basis_co = np.empty((len(me.vertices), 3), dtype=float) 755 me.vertices.foreach_get('co', basis_co.ravel()) 756 def get_sk_delta_coordinates(shape_key, out): 757 delta_coordinates = out 758 shape_key.data.foreach_get('co', delta_coordinates.ravel()) 759 delta_coordinates -= basis_co 760 return out 761 762 static_array_sq = np.empty((len(me.vertices), 3), dtype=float) 763 def get_lengths_squared(vectors, out): 764 np.power(vectors, 2, out=static_array_sq) 765 np.sum(static_array_sq, axis=1, out=out.ravel()) 766 return out 767 768 def write_morph(morph, name): 769 common.write_str(writer, 'morph') 770 common.write_str(writer, name) 771 writer.write(struct.pack('<i', len(morph))) 772 for v_index, vec, normal in morph: 773 vec = compat.convert_bl_to_cm_space(vec ) 774 normal = compat.convert_bl_to_cm_space(normal) 775 writer.write(struct.pack('<H', v_index)) 776 writer.write(struct.pack('<3f', *vec[:3])) 777 writer.write(struct.pack('<3f', *normal[:3])) 778 779 # accessing operator properties via "self.x" is SLOW, so store some here 780 self__export_shapekey_normals = self.export_shapekey_normals 781 self__use_shapekey_colors = self.use_shapekey_colors 782 self__shapekey_normals_blend = self.shapekey_normals_blend 783 self__scale = self.scale 784 785 co_diff_threshold = self.shapekey_threshold / 5 786 co_diff_threshold_squared = co_diff_threshold * co_diff_threshold 787 no_diff_threshold = self.shapekey_threshold * 10 788 no_diff_threshold_squared = no_diff_threshold * no_diff_threshold 789 790 # shared arrays 791 delta_coordinates = np.empty((len(me.vertices), 3), dtype=float) 792 vert_delta_normals = np.empty((len(me.vertices), 3), dtype=float) 793 loop_delta_normals = np.empty((len(me.loops ), 3), dtype=float) 794 795 delta_co_lensq = np.empty((len(me.vertices)), dtype=float) 796 delta_no_lensq = np.empty((len(me.vertices)), dtype=float) 797 798 if not self.export_shapekey_normals: 799 vert_delta_normals.fill(0) 800 delta_no_lensq.fill(0) 801 802 # HEAVY LOOP 803 for shape_key in me.shape_keys.key_blocks[1:]: 804 morph = [] 805 806 if self__export_shapekey_normals and self__use_shapekey_colors: 807 normals_color, attrubute_is_color = find_normals_attribute(f'{shape_key.name}_delta_normals') 808 809 if self__export_shapekey_normals: 810 if self__use_shapekey_colors and not normals_color is None: 811 sk_delta_normals = get_sk_delta_normals_from_attribute(normals_color, attrubute_is_color, out=vert_delta_normals) 812 elif me.has_custom_normals: 813 sk_delta_normals = get_sk_delta_normals_from_custom_normals(shape_key, out=vert_delta_normals) 814 sk_delta_normals *= self__shapekey_normals_blend 815 else: 816 sk_delta_normals = get_sk_delta_normals_from_normals(shape_key, out=vert_delta_normals) 817 sk_delta_normals *= self__shapekey_normals_blend 818 819 sk_no_lensq = get_lengths_squared(sk_delta_normals, out=delta_no_lensq) 820 else: 821 sk_delta_normals = vert_delta_normals 822 sk_no_lensq = delta_no_lensq 823 824 sk_co_diffs = get_sk_delta_coordinates(shape_key, out=delta_coordinates) 825 sk_co_diffs *= self__scale # scale before getting lengths 826 sk_co_lensq = get_lengths_squared(sk_co_diffs, out=delta_co_lensq) 827 828 # SUPER HEAVY LOOP 829 outvert_index = 0 830 for i in range(len(me.vertices)): 831 if sk_co_lensq[i] >= co_diff_threshold_squared or sk_no_lensq[i] >= no_diff_threshold_squared: 832 morph += [ (outvert_index+j, sk_co_diffs[i], sk_delta_normals[i]) for j in range(len(vert_uvs[i])) ] 833 else: 834 # ignore because change is too small (greatly lowers file size) 835 pass 836 outvert_index += len(vert_uvs[i]) 837 838 if prefs.skip_shapekey and not len(morph): 839 continue 840 else: 841 write_morph(morph, shape_key.name)
def
parse_triangles(self, bm, ob, uv_lay, vert_iuv, vert_indices):
849 def parse_triangles(self, bm, ob, uv_lay, vert_iuv, vert_indices): 850 def vert_index_from_loops(loops): 851 """vert_index generator""" 852 for loop in loops: 853 uv = loop[uv_lay].uv 854 v_index = loop.vert.index 855 vert_index = vert_iuv.get(hash((v_index, uv.x, uv.y))) 856 if vert_index is None: 857 vert_index = vert_indices.get(v_index, 0) 858 yield vert_index 859 860 triangles = [] 861 for mate_index, slot in enumerate(ob.material_slots): 862 tris_faces = [] 863 for face in bm.faces: 864 if face.material_index != mate_index: 865 continue 866 if len(face.verts) == 3: 867 tris_faces.extend(vert_index_from_loops(reversed(face.loops))) 868 elif len(face.verts) == 4 and self.is_convert_tris: 869 v1 = face.loops[0].vert.co - face.loops[2].vert.co 870 v2 = face.loops[1].vert.co - face.loops[3].vert.co 871 if v1.length < v2.length: 872 f1 = [0, 1, 2] 873 f2 = [0, 2, 3] 874 else: 875 f1 = [0, 1, 3] 876 f2 = [1, 2, 3] 877 faces, faces2 = [], [] 878 for i, vert_index in enumerate(vert_index_from_loops(reversed(face.loops))): 879 if i in f1: 880 faces.append(vert_index) 881 if i in f2: 882 faces2.append(vert_index) 883 tris_faces.extend(faces) 884 tris_faces.extend(faces2) 885 elif 5 <= len(face.verts) and self.is_convert_tris: 886 face_count = len(face.verts) - 2 887 888 tris = [] 889 seek_min, seek_max = 0, len(face.verts) - 1 890 for i in range(face_count): 891 if not i % 2: 892 tris.append([seek_min, seek_min + 1, seek_max]) 893 seek_min += 1 894 else: 895 tris.append([seek_min, seek_max - 1, seek_max]) 896 seek_max -= 1 897 898 tris_indexs = [[] for _ in range(len(tris))] 899 for i, vert_index in enumerate(vert_index_from_loops(reversed(face.loops))): 900 for tris_index, points in enumerate(tris): 901 if i in points: 902 tris_indexs[tris_index].append(vert_index) 903 904 tris_faces.extend(p for ps in tris_indexs for p in ps) 905 906 triangles.append(tris_faces) 907 return triangles
def
calc_tangents(self, cm_tris, cm_verts, cm_norms, cm_uvs):
909 def calc_tangents(self, cm_tris, cm_verts, cm_norms, cm_uvs): 910 count = len(cm_verts) 911 tan1 = [None] * count 912 tan2 = [None] * count 913 for i in range(0, count): 914 tan1[i] = mathutils.Vector((0, 0, 0)) 915 tan2[i] = mathutils.Vector((0, 0, 0)) 916 917 for tris in cm_tris: 918 tri_len = len(tris) 919 tri_idx = 0 920 while tri_idx < tri_len: 921 i1, i2, i3 = tris[tri_idx], tris[tri_idx + 1], tris[tri_idx + 2] 922 v1, v2, v3 = cm_verts[i1], cm_verts[i2], cm_verts[i3] 923 w1, w2, w3 = cm_uvs[i1], cm_uvs[i2], cm_uvs[i3] 924 925 a1 = v2 - v1 926 a2 = v3 - v1 927 s1 = w2 - w1 928 s2 = w3 - w1 929 930 r_inverse = (s1.x * s2.y - s2.x * s1.y) 931 932 if r_inverse != 0: 933 # print("i1 = {i1} i2 = {i2} i3 = {i3}".format(i1=i1, i2=i2, i3=i3)) 934 # print("v1 = {v1} v2 = {v2} v3 = {v3}".format(v1=v1, v2=v2, v3=v3)) 935 # print("w1 = {w1} w2 = {w2} w3 = {w3}".format(w1=w1, w2=w2, w3=w3)) 936 937 # print("a1 = {a1} a2 = {a2}".format(a1=a1, a2=a2)) 938 # print("s1 = {s1} s2 = {s2}".format(s1=s1, s2=s2)) 939 940 # print("r_inverse = ({s1x} * {s2y} - {s2x} * {s1y}) = {r_inverse}".format(r_inverse=r_inverse, s1x=s1.x, s1y=s1.y, s2x=s2.x, s2y=s2.y)) 941 942 r = 1.0 / r_inverse 943 sdir = mathutils.Vector(((s2.y * a1.x - s1.y * a2.x) * r, (s2.y * a1.y - s1.y * a2.y) * r, (s2.y * a1.z - s1.y * a2.z) * r)) 944 tan1[i1] += sdir 945 tan1[i2] += sdir 946 tan1[i3] += sdir 947 948 tdir = mathutils.Vector(((s1.x * a2.x - s2.x * a1.x) * r, (s1.x * a2.y - s2.x * a1.y) * r, (s1.x * a2.z - s2.x * a1.z) * r)) 949 tan2[i1] += tdir 950 tan2[i2] += tdir 951 tan2[i3] += tdir 952 953 tri_idx += 3 954 955 tangents = [None] * count 956 for i in range(0, count): 957 n = cm_norms[i] 958 ti = tan1[i] 959 t = (ti - n * n.dot(ti)).normalized() 960 961 c = n.cross(ti) 962 val = c.dot(tan2[i]) 963 w = 1.0 if val < 0 else -1.0 964 tangents[i] = (-t.x, t.y, t.z, w) 965 966 return tangents
def
select_no_weight_vertices(self, context, local_bone_name_indices):
968 def select_no_weight_vertices(self, context, local_bone_name_indices): 969 """ウェイトが割り当てられていない頂点を選択する""" 970 ob = context.active_object 971 me = ob.data 972 bpy.ops.object.mode_set(mode='EDIT') 973 bpy.ops.mesh.select_all(action='SELECT') 974 #bpy.ops.object.mode_set(mode='OBJECT') 975 context.tool_settings.mesh_select_mode = (True, False, False) 976 for vert in me.vertices: 977 for vg in vert.groups: 978 if len(ob.vertex_groups) <= vg.group: # Apparently a vertex can be assigned to a non-existent group. 979 continue 980 name = common.encode_bone_name(ob.vertex_groups[vg.group].name, self.is_convert_bone_weight_names) 981 if name in local_bone_name_indices and 0.0 < vg.weight: 982 vert.select = False 983 break 984 bpy.ops.object.mode_set(mode='EDIT')
ウェイトが割り当てられていない頂点を選択する
def
armature_bone_data_parser(self, context, ob):
986 def armature_bone_data_parser(self, context, ob): 987 """アーマチュアを解析してBoneDataを返す""" 988 arm = ob.data 989 990 pre_active = compat.get_active(context) 991 pre_mode = ob.mode 992 993 compat.set_active(context, ob) 994 bpy.ops.object.mode_set(mode='EDIT') 995 996 bones = [] 997 bone_name_indices = {} 998 already_bone_names = [] 999 bones_queue = arm.edit_bones[:] 1000 while len(bones_queue): 1001 bone = bones_queue.pop(0) 1002 1003 if not bone.parent: 1004 already_bone_names.append(bone.name) 1005 bones.append(bone) 1006 bone_name_indices[bone.name] = len(bone_name_indices) 1007 continue 1008 elif bone.parent.name in already_bone_names: 1009 already_bone_names.append(bone.name) 1010 bones.append(bone) 1011 bone_name_indices[bone.name] = len(bone_name_indices) 1012 continue 1013 1014 bones_queue.append(bone) 1015 1016 bone_data = [] 1017 for bone in bones: 1018 1019 # Also check for UnknownFlag for backwards compatibility 1020 is_scl_bone = bone['cm3d2_scl_bone'] if 'cm3d2_scl_bone' in bone \ 1021 else bone['UnknownFlag'] if 'UnknownFlag' in bone \ 1022 else 0 1023 parent_index = bone_name_indices[bone.parent.name] if bone.parent else -1 1024 1025 mat = bone.matrix.copy() 1026 1027 if bone.parent: 1028 mat = compat.convert_bl_to_cm_bone_rotation(mat) 1029 mat = compat.mul(bone.parent.matrix.inverted(), mat) 1030 mat = compat.convert_bl_to_cm_bone_space(mat) 1031 else: 1032 mat = compat.convert_bl_to_cm_bone_rotation(mat) 1033 mat = compat.convert_bl_to_cm_space(mat) 1034 1035 co = mat.to_translation() * self.scale 1036 rot = mat.to_quaternion() 1037 1038 #if bone.parent: 1039 # co.x, co.y, co.z = -co.y, -co.x, co.z 1040 # rot.w, rot.x, rot.y, rot.z = rot.w, rot.y, rot.x, -rot.z 1041 #else: 1042 # co.x, co.y, co.z = -co.x, co.z, -co.y 1043 # 1044 # fix_quat = compat.Z_UP_TO_Y_UP_QUAT #mathutils.Euler((0, 0, math.radians(-90)), 'XYZ').to_quaternion() 1045 # fix_quat2 = compat.BLEND_TO_OPENGL_QUAT #mathutils.Euler((math.radians(-90), 0, 0), 'XYZ').to_quaternion() 1046 # rot = compat.mul3(rot, fix_quat, fix_quat2) 1047 # #rot = compat.mul3(fix_quat2, rot, fix_quat) 1048 # 1049 # rot.w, rot.x, rot.y, rot.z = -rot.y, -rot.z, -rot.x, rot.w 1050 1051 # luvoid : I copied this from the Bone-Util Addon by trzr 1052 #if bone.parent: 1053 # co.x, co.y, co.z = -co.y, co.z, co.x 1054 # rot.w, rot.x, rot.y, rot.z = rot.w, rot.y, -rot.z, -rot.x 1055 #else: 1056 # co.x, co.y, co.z = -co.x, co.z, -co.y 1057 # 1058 # rot = compat.mul(rot, mathutils.Quaternion((0, 0, 1), math.radians(90))) 1059 # rot.w, rot.x, rot.y, rot.z = -rot.w, -rot.x, rot.z, -rot.y 1060 1061 #opengl_mat = compat.convert_blend_z_up_to_opengl_y_up_mat4(bone.matrix) 1062 # 1063 #if bone.parent: 1064 # opengl_mat = compat.mul(compat.convert_blend_z_up_to_opengl_y_up_mat4(bone.parent.matrix).inverted(), opengl_mat) 1065 # 1066 #co = opengl_mat.to_translation() * self.scale 1067 #rot = opengl_mat.to_quaternion() 1068 1069 data = { 1070 'name': common.encode_bone_name(bone.name, self.is_convert_bone_weight_names), 1071 'scl': is_scl_bone, 1072 'parent_index': parent_index, 1073 'co': co.copy(), 1074 'rot': rot.copy(), 1075 } 1076 scale = arm.edit_bones[bone.name].get('cm3d2_bone_scale') 1077 if scale: 1078 data['scale'] = scale 1079 bone_data.append(data) 1080 1081 bpy.ops.object.mode_set(mode=pre_mode) 1082 compat.set_active(context, pre_active) 1083 return bone_data
アーマチュアを解析してBoneDataを返す
@staticmethod
def
bone_data_parser(container):
1085 @staticmethod 1086 def bone_data_parser(container): 1087 """BoneData テキストをパースして辞書を要素とするリストを返す""" 1088 bone_data = [] 1089 bone_name_indices = {} 1090 for line in container: 1091 data = line.split(',') 1092 if len(data) < 5: 1093 continue 1094 1095 parent_name = data[2] 1096 if parent_name.isdigit(): 1097 parent_index = int(parent_name) 1098 else: 1099 parent_index = bone_name_indices.get(parent_name, -1) 1100 1101 bone_datum = { 1102 'name': data[0], 1103 'scl': int(data[1]), 1104 'parent_index': parent_index, 1105 'co': list(map(float, data[3].split())), 1106 'rot': list(map(float, data[4].split())), 1107 } 1108 # scale info (for version 2001 or later) 1109 if len(data) >= 7: 1110 if data[5] == '1': 1111 bone_scale = data[6] 1112 bone_datum['scale'] = list(map(float, bone_scale.split())) 1113 bone_data.append(bone_datum) 1114 bone_name_indices[data[0]] = len(bone_name_indices) 1115 return bone_data
BoneData テキストをパースして辞書を要素とするリストを返す
def
armature_local_bone_data_parser(self, ob):
1117 def armature_local_bone_data_parser(self, ob): 1118 """アーマチュアを解析してBoneDataを返す""" 1119 arm = ob.data 1120 1121 # XXX Instead of just adding all bones, only bones / bones-with-decendants 1122 # that have use_deform == True or mathcing vertex groups should be used 1123 bones = [] 1124 bone_name_indices = {} 1125 already_bone_names = [] 1126 bones_queue = arm.bones[:] 1127 while len(bones_queue): 1128 bone = bones_queue.pop(0) 1129 1130 if not bone.parent: 1131 already_bone_names.append(bone.name) 1132 bones.append(bone) 1133 bone_name_indices[bone.name] = len(bone_name_indices) 1134 continue 1135 elif bone.parent.name in already_bone_names: 1136 already_bone_names.append(bone.name) 1137 bones.append(bone) 1138 bone_name_indices[bone.name] = len(bone_name_indices) 1139 continue 1140 1141 bones_queue.append(bone) 1142 1143 local_bone_data = [] 1144 for bone in bones: 1145 mat = bone.matrix_local.copy() 1146 mat = compat.mul(mathutils.Matrix.Scale(-1, 4, (1, 0, 0)), mat) 1147 mat = compat.convert_bl_to_cm_bone_rotation(mat) 1148 pos = mat.translation.copy() 1149 1150 mat.transpose() 1151 mat.row[3] = (0.0, 0.0, 0.0, 1.0) 1152 pos = compat.mul(mat.to_3x3(), pos) 1153 pos *= -self.scale 1154 mat.translation = pos 1155 mat.transpose() 1156 1157 #co = mat.to_translation() * self.scale 1158 #rot = mat.to_quaternion() 1159 # 1160 #co.rotate(rot.inverted()) 1161 #co.x, co.y, co.z = co.y, co.x, -co.z 1162 # 1163 #fix_quat = mathutils.Euler((0, 0, math.radians(-90)), 'XYZ').to_quaternion() 1164 #rot = compat.mul(rot, fix_quat) 1165 #rot.w, rot.x, rot.y, rot.z = -rot.z, -rot.y, -rot.x, rot.w 1166 # 1167 #co_mat = mathutils.Matrix.Translation(co) 1168 #rot_mat = rot.to_matrix().to_4x4() 1169 #mat = compat.mul(co_mat, rot_mat) 1170 # 1171 #copy_mat = mat.copy() 1172 #mat[0][0], mat[0][1], mat[0][2], mat[0][3] = copy_mat[0][0], copy_mat[1][0], copy_mat[2][0], copy_mat[3][0] 1173 #mat[1][0], mat[1][1], mat[1][2], mat[1][3] = copy_mat[0][1], copy_mat[1][1], copy_mat[2][1], copy_mat[3][1] 1174 #mat[2][0], mat[2][1], mat[2][2], mat[2][3] = copy_mat[0][2], copy_mat[1][2], copy_mat[2][2], copy_mat[3][2] 1175 #mat[3][0], mat[3][1], mat[3][2], mat[3][3] = copy_mat[0][3], copy_mat[1][3], copy_mat[2][3], copy_mat[3][3] 1176 1177 mat_array = [] 1178 for vec in mat: 1179 mat_array.extend(vec[:]) 1180 1181 local_bone_data.append({ 1182 'name': common.encode_bone_name(bone.name, self.is_convert_bone_weight_names), 1183 'matrix': mat_array, 1184 }) 1185 return local_bone_data
アーマチュアを解析してBoneDataを返す
@staticmethod
def
local_bone_data_parser(container):
1187 @staticmethod 1188 def local_bone_data_parser(container): 1189 """LocalBoneData テキストをパースして辞書を要素とするリストを返す""" 1190 local_bone_data = [] 1191 for line in container: 1192 data = line.split(',') 1193 if len(data) != 2: 1194 continue 1195 local_bone_data.append({ 1196 'name': data[0], 1197 'matrix': list(map(float, data[1].split())), 1198 }) 1199 return local_bone_data
LocalBoneData テキストをパースして辞書を要素とするリストを返す
@staticmethod
def
indexed_data_generator(container, prefix='', max_index=387420489, max_pass=50):
1201 @staticmethod 1202 def indexed_data_generator(container, prefix='', max_index=9**9, max_pass=50): 1203 """コンテナ内の数値インデックスをキーに持つ要素を昇順に返すジェネレーター""" 1204 pass_count = 0 1205 for i in range(max_index): 1206 name = prefix + str(i) 1207 if name not in container: 1208 pass_count += 1 1209 if max_pass < pass_count: 1210 return 1211 continue 1212 yield container[name]
コンテナ内の数値インデックスをキーに持つ要素を昇順に返すジェネレーター
Inherited Members
- bpy_types.Operator
- as_keywords
- poll_message_set
- builtins.bpy_struct
- keys
- values
- get
- pop
- as_pointer
- keyframe_insert
- keyframe_delete
- driver_add
- driver_remove
- is_property_set
- property_unset
- is_property_readonly
- is_property_overridable_library
- property_overridable_library_set
- path_resolve
- path_from_id
- type_recast
- bl_rna_get_subclass_py
- bl_rna_get_subclass
- id_properties_ensure
- id_properties_clear
- id_properties_ui
- id_data